본문으로 건너뛰기

WebRTC 화상통화 구현

WebRTC 화상통화 구현ConnectBase WebRTC API로 1:1 및 그룹 화상통화를 구현합니다

고급실시간16초읽기 약 3분업데이트 2026-04-14

ConnectBase WebRTC API로 1:1 및 그룹 화상통화를 구현합니다

이 튜토리얼에서 배울 내용
WebRTC 연결 자동 초기화
카메라/마이크 스트림 관리
통화방 생성/참여
참가자 영상 관리

WebRTC 화상통화 구현

WebRTC(Web Real-Time Communication)는 브라우저 간 직접 음성/영상을 주고받는 기술입니다. 하지만 직접 구현하려면 STUN/TURN 서버, 시그널링 서버, ICE candidate 교환 등 복잡한 과정이 필요합니다. ConnectBase WebRTC API를 사용하면 이 모든 과정을 SDK가 처리해줍니다.

완성된 기능

  • WebRTC 연결 자동 초기화 (STUN/TURN 서버 설정 불필요)
  • 카메라/마이크 미디어 스트림 획득
  • 통화방 생성 및 참여
  • 참가자 입장/퇴장 시 영상 스트림 자동 관리

사전 준비

  1. ConnectBase 콘솔에서 실시간 → WebRTC 서비스를 활성화합니다
    • 왼쪽 메뉴에서 실시간을 클릭합니다
    • WebRTC 탭에서 토글을 ON으로 켭니다
  2. 테스트 시 HTTPS 환경이 필요합니다 (카메라/마이크 권한은 HTTPS에서만 동작)
    • 로컬 개발 시 localhost는 예외적으로 HTTP에서도 동작합니다

1. 프로젝트 설정

bash
npm create vite@latest video-call -- --template react-ts
cd video-call
npm install connectbase-client

.env 파일을 만들고 Public Key를 입력합니다:

VITE_CONNECT_BASE_PUBLIC_KEY=cb_pk_여기에_퍼블릭키_입력

2. SDK 설정

src/lib/connectbase.ts:

typescript
import { ConnectBase } from 'connectbase-client'

export const cb = new ConnectBase({
  publicKey: import.meta.env.VITE_CONNECT_BASE_PUBLIC_KEY,
  appId: import.meta.env.VITE_APP_ID,  // WebRTC 는 appId 가 필요합니다 (ICE 서버 조회용)
})

3. WebRTC 훅 만들기

카메라/마이크 연결, 방 생성/참여, 참가자 관리를 하나의 훅으로 묶습니다.

SDK 구조: cb.webrtc.connect({ roomId, userId, isBroadcaster, localStream }) 가 시그널링 WebSocket 연결을 담당합니다. 같은 roomId 로 connect 하면 자동으로 같은 방에 참여 — 별도 createRoom / joinRoom 메서드는 없으며 roomId 명명으로 구분합니다. 참가자 입장/퇴장은 onPeerJoined / onPeerLeft 콜백, 원격 스트림은 onRemoteStream 콜백으로 관리합니다.

src/hooks/useWebRTC.ts:

typescript
import { useState, useRef, useEffect } from 'react'
import { cb } from '../lib/connectbase'

interface RemotePeer { peerId: string; stream: MediaStream }

export function useWebRTC() {
  const [peers, setPeers] = useState<RemotePeer[]>([])
  const [connected, setConnected] = useState(false)
  const localVideoRef = useRef<HTMLVideoElement>(null)
  const localStreamRef = useRef<MediaStream | null>(null)

  // 콜백 등록 — 컴포넌트 마운트 시 한 번 (반환된 unsubscribe 로 cleanup)
  useEffect(() => {
    const offJoined = cb.webrtc.onPeerJoined((peerId) => {
      // peer 가 들어왔지만 stream 은 onRemoteStream 에서 받음
      // 필요하면 여기에 정보만 미리 저장
    })
    const offStream = cb.webrtc.onRemoteStream((peerId, stream) => {
      setPeers(prev => [...prev.filter(p => p.peerId !== peerId), { peerId, stream }])
    })
    const offLeft = cb.webrtc.onPeerLeft((peerId) => {
      setPeers(prev => prev.filter(p => p.peerId !== peerId))
    })
    const offState = cb.webrtc.onStateChange((state) => {
      setConnected(state === 'connected')
    })
    return () => { offJoined(); offStream(); offLeft(); offState() }
  }, [])

  // 카메라/마이크 켜기
  const startMedia = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    localStreamRef.current = stream
    if (localVideoRef.current) localVideoRef.current.srcObject = stream
    return stream
  }

  // 방에 참여 (같은 roomId 로 양쪽이 connect 하면 자동 매칭됨)
  const join = async (roomId: string, userId: string) => {
    const localStream = await startMedia()
    await cb.webrtc.connect({
      roomId,                    // 공유할 통화방 ID (예: 'call:abc-123')
      userId,                    // 본인을 식별할 ID
      isBroadcaster: true,       // 영상/음성 송신자 — 모든 참가자가 송신하면 true
      localStream,
    })
  }

  // 연결 종료
  const leave = () => {
    cb.webrtc.disconnect()
    localStreamRef.current?.getTracks().forEach(t => t.stop())
  }

  return { localVideoRef, peers, connected, join, leave }
}

4. 화상통화 UI 만들기

내 영상과 다른 참가자들의 영상을 격자 레이아웃으로 표시합니다.

src/components/VideoCall.tsx:

tsx
import { useState } from 'react'
import { useWebRTC } from '../hooks/useWebRTC'

export function VideoCall() {
  const { localVideoRef, peers, connected, join, leave } = useWebRTC()
  const [roomId, setRoomId] = useState('demo-room-1')
  const userId = 'user-' + Math.random().toString(36).slice(2, 8)

  return (
    <div>
      <div style={{ marginBottom: 12 }}>
        <input value={roomId} onChange={(e) => setRoomId(e.target.value)} />
        <button onClick={() => join(roomId, userId)} disabled={connected}>참여</button>
        <button onClick={leave} disabled={!connected}>나가기</button>
        <span style={{ marginLeft: 8 }}>{connected ? '연결됨' : '대기'}</span>
      </div>

      {/* 비디오 격자 — 내 영상 + 다른 peer 영상 */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
        {/* 내 영상 — muted 로 에코 방지 */}
        <video ref={localVideoRef} autoPlay muted playsInline style={{ width: '100%' }} />

        {/* 원격 peer 영상 */}
        {peers.map((p) => (
          <video
            key={p.peerId}
            ref={(el) => { if (el) el.srcObject = p.stream }}
            autoPlay
            playsInline
            style={{ width: '100%' }}
          />
        ))}
      </div>
    </div>
  )
}

💡 내 영상에 muted 속성을 꼭 넣어야 합니다. 그렇지 않으면 내 마이크 소리가 내 스피커로 나와 에코가 발생합니다.

5. 실행하기

bash
npm run dev
  1. 브라우저에서 http://localhost:5173 을 엽니다
  2. 카메라/마이크 접근 허용 팝업이 뜨면 허용을 클릭합니다
  3. 방 ID 를 입력하고 "참여" 버튼을 클릭합니다
  4. 다른 브라우저 탭이나 기기에서 같은 방 ID 로 참여하여 테스트합니다

다음 단계

  • 화면 공유navigator.mediaDevices.getDisplayMedia() 로 받은 스트림을 connect({ localStream }) 에 전달
  • 음소거/카메라 끄기localStreamRef.current.getAudioTracks()[0].enabled = false
  • 에러 처리cb.webrtc.onError((err) => ...) 콜백으로 시그널링/ICE 실패 감지
  • 수신 전용 모드isBroadcaster: false + localStream 생략 시 viewer 로만 참여
  • 통화 종료cb.webrtc.disconnect() 호출 (위 leave 함수에서 이미 처리)

이 튜토리얼이 도움이 됐나요?