Skip to content

✍️ 필사 모드: WebRTC 완전 가이드 2025: 실시간 비디오/오디오 통신, P2P, STUN/TURN, 시그널링, SFU/MCU

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

TL;DR

  • WebRTC = P2P + 미디어 + 데이터 — 브라우저, 모바일, 서버 어디서나 통신
  • 3가지 핵심 API: getUserMedia (카메라/마이크), RTCPeerConnection (P2P 연결), RTCDataChannel (임의 데이터)
  • NAT 트래버설이 핵심: STUN(IP 발견) → TURN(중계, 비싼 마지막 수단) → ICE(최적 경로 선택)
  • 3명 이상이면 SFU 필수: P2P는 N²; SFU는 중앙 서버가 미디어 라우팅
  • 실전 SFU 라이브러리: mediasoup(Node), Janus(C), Pion(Go), LiveKit(Go)

1. WebRTC의 비밀

1.1 한 줄 정의

WebRTC = 브라우저에서 플러그인 없이 P2P 음성/비디오/데이터 통신을 가능케 하는 W3C 표준.

WebRTC가 등장하기 전:

  • 비디오 채팅 = Flash, Java Applet, ActiveX (모두 사라짐)
  • 음성 통화 = Skype 같은 별도 앱
  • 화면 공유 = 전용 소프트웨어

WebRTC의 약속: 단 몇 줄의 JavaScript로 Skype 수준의 통신 구현.

1.2 WebRTC의 3가지 API

API용도
getUserMedia카메라/마이크 접근
RTCPeerConnection피어 간 연결 + 미디어 송수신
RTCDataChannel임의 데이터 P2P 전송 (파일, 게임 상태)

1.3 가장 단순한 예시

// 1. 카메라 접근
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
})
document.querySelector('video').srcObject = stream

// 2. P2P 연결
const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
})

// 3. 로컬 스트림 추가
stream.getTracks().forEach(track => pc.addTrack(track, stream))

// 4. 원격 스트림 수신
pc.ontrack = (event) => {
  document.querySelector('#remote-video').srcObject = event.streams[0]
}

이게 전부입니다. 단, 시그널링NAT 트래버설이 숨어있습니다.


2. 시그널링 (Signaling)

2.1 시그널링이란?

WebRTC는 P2P 연결을 만들지만, 연결을 만들기 전에 정보를 교환해야 합니다:

  • 내 IP/포트는?
  • 어떤 코덱을 지원해?
  • DTLS 핑거프린트는?

이 정보 교환을 시그널링이라고 합니다. WebRTC 표준은 시그널링 방법을 정의하지 않습니다 — 개발자가 구현.

2.2 일반적 시그널링 = WebSocket

// 시그널링 서버 (Node.js)
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', (ws) => {
  ws.on('message', (msg) => {
    // 다른 클라이언트들에게 브로드캐스트
    wss.clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(msg)
      }
    })
  })
})

2.3 SDP — Session Description Protocol

피어들이 교환하는 정보는 SDP 형식:

v=0
o=- 4611731400430051336 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 0 8
a=rtcp-mux
a=rtpmap:111 opus/48000/2
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
a=rtcp-mux
a=rtpmap:96 VP8/90000

이는 "내가 지원하는 코덱, 내 RTP 설정, 보안 키 등"을 담고 있습니다.

2.4 Offer/Answer 모델

// Peer A (caller)
const offer = await pcA.createOffer()
await pcA.setLocalDescription(offer)
signalingChannel.send({ type: 'offer', sdp: offer.sdp })

// Peer B (callee)
signalingChannel.on('offer', async (offer) => {
  await pcB.setRemoteDescription(offer)
  const answer = await pcB.createAnswer()
  await pcB.setLocalDescription(answer)
  signalingChannel.send({ type: 'answer', sdp: answer.sdp })
})

// Peer A receives answer
signalingChannel.on('answer', async (answer) => {
  await pcA.setRemoteDescription(answer)
})

// 양쪽: ICE 후보 교환
pcA.onicecandidate = (event) => {
  if (event.candidate) {
    signalingChannel.send({ type: 'ice', candidate: event.candidate })
  }
}

3. NAT 트래버설 — WebRTC의 가장 어려운 부분

3.1 NAT 문제

대부분의 클라이언트는 NAT 뒤에 있습니다:

  • 가정용 라우터
  • 회사 방화벽
  • 모바일 캐리어 네트워크

NAT 뒤의 클라이언트는 자신의 공인 IP를 모릅니다. 다른 클라이언트가 직접 연결할 수도 없습니다.

3.2 STUN — Simple Traversal of UDP through NAT

STUN 서버가 클라이언트의 공인 IP/포트를 알려줍니다.

[Client A]STUN server → "당신의 공인 IP는 1.2.3.4:5678"
[Client B]STUN server → "당신의 공인 IP는 5.6.7.8:9012"

이 정보를 시그널링으로 교환하면, 클라이언트끼리 직접 연결 시도 가능 (UDP hole punching).

무료 STUN 서버: stun:stun.l.google.com:19302

3.3 TURN — 중계 서버

STUN으로 안 되는 경우 (대칭 NAT, 엄격한 방화벽):

  • 모든 트래픽을 TURN 서버가 중계
  • 중계 비용 = 비싸다 (대역폭 + CPU)
  • 마지막 수단

coturn 같은 오픈소스 TURN 서버 운영 가능.

3.4 ICE — 모든 것을 합치기

ICE (Interactive Connectivity Establishment):

  1. 모든 후보 IP/포트 수집 (로컬, STUN, TURN)
  2. 각 후보 쌍에 대해 연결 시도
  3. 가장 빠른 경로 선택

ICE 후보 우선순위:

  1. 호스트 후보 (로컬 IP) - 최고
  2. server reflexive (STUN으로 발견) - 좋음
  3. 릴레이 (TURN) - 최후

3.5 STUN/TURN 서버 설정

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:my-turn-server.com:3478',
      username: 'user',
      credential: 'password'
    }
  ]
})

4. 미디어 서버 — P2P를 넘어서

4.1 P2P의 한계

3명 회의 = 각자 2개 연결 = 총 6개 스트림 = N(N-1) 연결.

참여자연결 수송신 스트림
2명12
5명1020
10명4590
50명1,2252,450

10명만 되어도 각 클라이언트가 9개 스트림 송신 → 대역폭 폭증, 배터리 소모, 모바일 불가능.

4.2 SFU (Selective Forwarding Unit)

해결: 중앙 서버가 각 참여자의 스트림을 받고 다른 모두에게 전달.

[A] ─stream─→ [SFU] ─stream─→ [B]
                  ├─stream─→ [C]
                  └─stream─→ [D]

각 클라이언트는 1개 송신, N-1개 수신. 송신 측 부하 절감.

SFU의 장점:

  • 인코딩 1번만 (각 참여자가)
  • 서버는 라우팅만 (디코딩 X) → 가벼움
  • 셀렉티브 포워딩 (저해상도/고해상도 선택)
  • Simulcast 지원

4.3 MCU (Multipoint Control Unit)

또 다른 옵션: 서버가 모든 스트림을 합성하여 단일 스트림 전송.

장점: 클라이언트가 1개만 받음 (가장 가벼움). 단점: 서버에서 디코딩+합성+인코딩 → 매우 비쌈, 지연시간 증가.

4.4 비교

P2PSFUMCU
참여자1:1다수다수
클라이언트 부하낮음보통매우 낮음
서버 부하없음보통매우 높음
비용0 (TURN 제외)대역폭대역폭+CPU+GPU
지연시간가장 낮음낮음보통
보안E2EE 가능서버가 봄서버가 봄
사용 사례1:1 통화화상회의방송

4.5 SFU 라이브러리

라이브러리언어특징
mediasoupNode.js + C++가장 인기, Discord 사용
JanusC모듈식, Pion보다 무거움
PionGo순수 Go, 가벼움, 임베디드 가능
LiveKitGo매니지드 서비스 + 오픈소스
Jitsi VideobridgeJavaJitsi Meet의 핵심
GaleneGo단순
OpenViduJavaKurento 기반

4.6 mediasoup 예시

const mediasoup = require('mediasoup')

// Worker 생성 (별도 프로세스)
const worker = await mediasoup.createWorker()
const router = await worker.createRouter({
  mediaCodecs: [
    { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
    { kind: 'video', mimeType: 'video/VP8', clockRate: 90000 }
  ]
})

// Producer (스트림 송신자)
const transport = await router.createWebRtcTransport({...})
const producer = await transport.produce({ kind: 'video', rtpParameters })

// Consumer (스트림 수신자)
const consumerTransport = await router.createWebRtcTransport({...})
const consumer = await consumerTransport.consume({
  producerId: producer.id,
  rtpCapabilities
})

mediasoup의 핵심 개념:

  • Router: 같은 미디어 라우팅 그룹 (방)
  • Transport: 클라이언트와의 연결
  • Producer: 미디어 송신자
  • Consumer: 미디어 수신자

5. 데이터 채널 — RTCDataChannel

5.1 사용 사례

  • 파일 전송 (P2P, 서버 거치지 않음)
  • 게임 상태 (낮은 지연시간)
  • 채팅 메시지 (시그널링과 별도)
  • CRDT 동기화 (Yjs y-webrtc)

5.2 사용법

// 송신자
const dc = pc.createDataChannel('my-channel', {
  ordered: true,           // 순서 보장
  maxRetransmits: 3        // 재전송 제한
})

dc.onopen = () => {
  dc.send('Hello!')
  dc.send(new Uint8Array([1, 2, 3]))  // 바이너리도 가능
}

// 수신자
pc.ondatachannel = (event) => {
  const channel = event.channel
  channel.onmessage = (e) => console.log('Received:', e.data)
}

5.3 옵션

옵션설명
ordered: true순서 보장 (TCP-like)
ordered: false순서 무시 (UDP-like, 빠름)
maxRetransmits: N재전송 N회
maxPacketLifeTime: ms시간 내 못 가면 포기

게임처럼 최신 정보가 중요하면 ordered: false, maxRetransmits: 0 (UDP-like).


6. 미디어 품질 최적화

6.1 Adaptive Bitrate

WebRTC는 자동으로 네트워크 상황에 맞춰 비트레이트를 조정합니다 (google-congestion-control).

수동 제어:

const sender = pc.getSenders().find(s => s.track.kind === 'video')
const params = sender.getParameters()
params.encodings[0].maxBitrate = 1000000  // 1 Mbps
await sender.setParameters(params)

6.2 Simulcast

같은 비디오를 여러 해상도/비트레이트로 동시 전송. SFU가 수신자에 따라 선택.

const transceiver = pc.addTransceiver(videoTrack, {
  sendEncodings: [
    { rid: 'q', maxBitrate: 100000, scaleResolutionDownBy: 4 },  // 240p
    { rid: 'h', maxBitrate: 500000, scaleResolutionDownBy: 2 },  // 480p
    { rid: 'f', maxBitrate: 1500000 }                             // 720p
  ]
})

효과: 모바일 사용자는 240p, 데스크톱은 720p — 같은 회의에서 각자 최적.

6.3 SVC (Scalable Video Coding)

Simulcast의 진화. 단일 스트림에 여러 레이어를 인코딩.

  • VP9, AV1이 SVC 지원
  • 서버에서 레이어 선택 가능 (재인코딩 X)
  • 대역폭 절감

6.4 코덱

코덱라이선스품질호환성
VP8무료보통모든 브라우저
VP9무료좋음대부분
AV1무료최고점차 확산
H.264라이선스 (대부분 무료)좋음모든 장치 (하드웨어 가속)
H.265라이선스매우 좋음제한적

WebRTC 권장: VP8 (호환성), VP9 (품질), AV1 (미래).


7. 음성 처리

7.1 자동 처리

WebRTC가 자동 적용:

  • 에코 캔슬링 (AEC)
  • 노이즈 억제 (NS)
  • 자동 게인 컨트롤 (AGC)
const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
})

7.2 Opus 코덱

WebRTC의 표준 음성 코덱:

  • 6 kbps ~ 510 kbps 가변
  • 고품질 (음악도 가능)
  • 저지연 (5ms ~ 60ms)
  • 에러 복구 (FEC, PLC)

7.3 음성 처리 라이브러리

  • Krisp — AI 노이즈 제거 (상용)
  • RNNoise — 무료 RNN 기반 (Mozilla)
  • WebRTC VAD — Voice Activity Detection

8. 보안

8.1 DTLS-SRTP

WebRTC는 모든 미디어가 암호화됩니다:

  • DTLS — 키 교환 (TLS over UDP)
  • SRTP — 미디어 암호화

선택권 없음 — 평문 미디어 불가능. 시큐어 by default.

8.2 End-to-End Encryption (E2EE)

기본: SFU는 미디어를 봅니다 (라우팅만 하지만 복호화 가능).

E2EE: 클라이언트끼리만 알 수 있는 키로 미디어를 한 번 더 암호화.

WebRTC Insertable Streams API:

const sender = pc.getSenders()[0]
const stream = sender.createEncodedStreams()
const transformStream = new TransformStream({
  transform(encodedFrame, controller) {
    // 추가 암호화
    encryptFrame(encodedFrame)
    controller.enqueue(encodedFrame)
  }
})
stream.readable.pipeThrough(transformStream).pipeTo(stream.writable)

제약: SFU는 비트레이트 기반 라우팅만 가능 (내용 모름). Simulcast 등 일부 기능 제약.

사용: Whereby, Galaxy 회의 (Apple FaceTime은 자체 프로토콜로 E2EE).

8.3 인증과 권한

WebRTC 자체는 신원 검증 안 함. 시그널링 레벨에서 인증:

  • JWT 토큰
  • OAuth
  • 짧은 룸 코드

9. 프로덕션 운영

9.1 모니터링

const stats = await pc.getStats()
stats.forEach(report => {
  if (report.type === 'inbound-rtp') {
    console.log({
      packetsLost: report.packetsLost,
      jitter: report.jitter,
      bytesReceived: report.bytesReceived,
      framesPerSecond: report.framesPerSecond
    })
  }
})

핵심 지표:

  • 패킷 손실률 (>3%면 문제)
  • Jitter (>30ms면 끊김)
  • Round-trip time (RTT)
  • 비트레이트 (실제 vs 목표)
  • 프레임 레이트

9.2 디버깅 도구

  • chrome://webrtc-internals — Chrome의 내장 도구
  • about:webrtc — Firefox
  • webrtc-stats.js — 통계 라이브러리

9.3 일반적 문제

증상원인해결
연결 실패NAT 통과 못 함TURN 서버 확인
끊김네트워크 불안정비트레이트 낮춤, FEC
에코AEC 비활성echoCancellation: true
검은 화면카메라 권한권한 다이얼로그 확인
음성만 안 나옴자동 재생 차단사용자 제스처 후 재생

9.4 확장성

수십~수백 명: SFU 1대로 충분 수천 명: SFU 클러스터 + 로드 밸런서 수만 명: 캐스케이딩 SFU + Edge 노드


10. 실전 — Discord, Google Meet은 어떻게 만들까?

10.1 Discord (mediasoup)

  • mediasoup + Elixir 시그널링
  • 글로벌 SFU 분산 (낮은 지연시간)
  • Opus 음성 + 화면 공유
  • ~1000명 음성 채널 지원

10.2 Google Meet (자체 구현)

  • 자체 미디어 서버
  • VP9 SVC 사용
  • AI 기반 노이즈 제거
  • 100명+ 참여자 지원

10.3 Zoom (자체 프로토콜)

  • WebRTC가 아닌 자체 프로토콜
  • 더 효율적 (커스텀)
  • 호환성을 위해 WebRTC도 지원

10.4 LiveKit (오픈소스 + 매니지드)

import { Room } from 'livekit-client'

const room = new Room()
await room.connect('wss://my-livekit-server.com', token)

await room.localParticipant.enableCameraAndMicrophone()

room.on('participantConnected', (p) => {
  console.log('Joined:', p.identity)
})

LiveKit는 SFU + 클라이언트 SDK + 매니지드 서비스를 모두 제공. WebRTC 복잡도를 추상화.


퀴즈

1. WebRTC에서 시그널링이 필요한 이유는?

: WebRTC는 P2P 연결을 만들지만, 연결 전 정보 교환이 필요합니다: (1) 내 IP/포트는 무엇인가? (2) 어떤 코덱을 지원하는가? (3) DTLS 핑거프린트는? 이 정보를 SDP(Session Description Protocol) 형식으로 교환합니다. WebRTC 표준은 시그널링 방법을 정의하지 않아 개발자가 구현해야 합니다 (보통 WebSocket).

2. STUN과 TURN의 차이는?

: STUN은 클라이언트가 자신의 공인 IP/포트를 발견하게 해줍니다 — 매우 가벼운 서버. TURN은 STUN으로 연결이 안 될 때 (대칭 NAT, 엄격한 방화벽) 모든 미디어 트래픽을 중계합니다 — 비싼 마지막 수단. 일반적으로 80% 사용자는 STUN으로 충분, 20%는 TURN 필요. coturn 오픈소스 TURN 서버를 직접 운영하면 비용 절감.

3. SFU와 MCU의 차이는?

: **SFU (Selective Forwarding Unit)**는 미디어를 받아 다른 참여자들에게 라우팅만 합니다 (디코딩 X). 가볍고, simulcast 지원. **MCU (Multipoint Control Unit)**는 모든 스트림을 디코딩+합성+재인코딩하여 단일 스트림 전송. 클라이언트는 가벼우나 서버 부하가 매우 큼. 현대 화상회의는 거의 SFU를 사용 (Discord, Meet, Zoom 모두). MCU는 방송이나 매우 제한된 클라이언트에서.

4. P2P가 3명 이상에서 안 되는 이유는?

: N(N-1) 연결이 필요합니다. 10명이면 90개 송신 스트림 = 각 클라이언트가 9개 스트림 송신. 대역폭 폭증, 배터리 소모, 모바일 불가능. SFU는 각 클라이언트가 1개 송신, N-1개 수신으로 줄여줍니다. SFU의 추가 이점: simulcast (수신자별 해상도 선택), 인코딩 1번만, 서버는 라우팅만.

5. WebRTC의 E2EE가 어려운 이유는?

: 기본 WebRTC는 DTLS-SRTP로 클라이언트-서버 구간만 암호화합니다. SFU가 미디어를 라우팅하려면 복호화해야 합니다 (이론적으로 SFU 운영자가 미디어를 볼 수 있음). 진정한 E2EE는 Insertable Streams API로 클라이언트끼리만 아는 키로 추가 암호화합니다. 단, SFU는 콘텐츠를 모르므로 simulcast 같은 기능에 제약. Whereby, Apple FaceTime이 E2EE를 구현했습니다.


참고 자료

현재 단락 (1/352)

- **WebRTC = P2P + 미디어 + 데이터** — 브라우저, 모바일, 서버 어디서나 통신

작성 글자: 0원문 글자: 10,715작성 단락: 0/352