결론 먼저 — 단계별 실패 위치를 알면 디버깅이 빨라진다
| 단계 | 이름 | 대표 실패 원인 |
|---|---|---|
| 1 | Signaling 채널 수립 | WebSocket 인증/CORS/방화벽 |
| 2 | SDP Offer/Answer 교환 | codec 미스매치, m-line 순서 |
| 3 | ICE candidate 수집 | STUN 서버 도달 실패 |
| 4 | ICE connectivity check | 방화벽, NAT 종류 비대칭 |
| 5 | DTLS handshake | 인증서, 시간 동기 (NTP) |
| 6 | SRTP 키 교환 → 미디어 흐름 | codec 협상 미스 |
“연결이 안 된다”는 신고는 6단계 중 어디서 멈췄는지부터 좁혀야 한다. 그래야 점검 시간이 분 단위가 된다.
단계 1 — Signaling 채널 수립
WebRTC 표준은 시그널링 프로토콜을 정해두지 않았다. 보통 WebSocket이나 HTTP long-poll을 쓴다. 이 단계에서 실패하면 “방을 만들 수도 없다”가 된다. 가장 흔한 원인은 WebSocket 인증 토큰 만료, CORS 미스, 회사 방화벽이 80/443 외 포트를 차단하는 경우다.
점검: 브라우저 DevTools의 Network 탭에서 WebSocket이 101 Switching Protocols까지 갔는지만 확인하면 된다.
단계 2 — SDP Offer / Answer 교환
Caller가 createOffer() → SDP를 시그널링 채널로 보냄. Callee가 setRemoteDescription() 후 createAnswer(). 이 단계의 핵심은 양쪽이 합의한 codec, payload type, m-line 순서다.
실패 패턴 둘:
- 한쪽이 Opus만 지원하는데 다른 쪽이 G.711만 지원하면 audio m-line이 합의되지 않는다.
- renegotiation 시 m-line 순서가 바뀌면 Chromium이 거부한다 (
InvalidModificationError).
단계 3 — ICE candidate 수집
setLocalDescription()이 끝나면 ICE 에이전트가 후보를 수집하기 시작한다. 후보의 종류는 세 가지다.
- host — 단말의 로컬 IP
- srflx (server reflexive) — STUN 서버가 본 단말의 공인 IP
- relay — TURN 서버를 경유하는 주소
이 단계에서 흔한 실패는 STUN 서버에 도달하지 못하는 경우다. 사내 네트워크에서 외부 UDP가 막혀있으면 srflx 후보가 비고, NAT 뒤 단말끼리 연결할 길이 사라진다.
단계 4 — ICE connectivity check
양쪽이 후보를 교환한 뒤, 모든 (local × remote) 페어에 대해 STUN Binding Request를 주고받으며 실제로 통신이 되는 페어를 찾는다. 처음 성공한 페어가 nominated pair가 되고, 거기로 미디어가 흐른다.
이 단계에서 멈추는 경우가 가장 디버깅이 까다롭다. 보통 NAT 종류의 비대칭(예: 한쪽 Symmetric NAT)이나 UDP 차단이 원인이고, TURN으로 fall-through 해야 풀린다. 다음 글에서 NAT 종류별 성공률 매트릭스를 다룬다.
단계 5 — DTLS handshake
ICE 페어가 정해지면 그 위에서 DTLS 핸드셰이크가 시작된다. WebRTC는 self-signed 인증서를 쓰는 게 표준이고, 인증서 fingerprint를 SDP에 박아 양쪽이 비교한다.
의외로 시간 동기 (NTP)가 안 맞는 서버에서 DTLS 협상이 실패하는 경우가 있다. 인증서 not-before/not-after를 검증할 때 시계가 어긋나면 거부된다.
단계 6 — SRTP 키 교환 → 미디어 흐름
DTLS-SRTP extension으로 SRTP 마스터 키를 도출한다. 이때부터 RTP 패킷이 흐르기 시작한다. 이 단계에서 실패하면 보통 codec 협상이 잘못된 경우다. SDP에는 합의돼 있는데 실제 패킷의 payload type이 다르거나, MIME이 어긋났을 때 수신측 디코더가 침묵한다.
개인 메모 — “어디서 멈췄나”부터 묻는 습관
WebRTC를 처음 만졌을 때 가장 어려웠던 점은 “안 됨”이라는 신호가 너무 모호하다는 것이었다. 화면이 검은 채로 멈춰 있고 콘솔에는 별다른 에러가 없는 상황이 흔했다. 그때 누군가가 “그게 ICE에서 멈춘 거냐, DTLS에서 멈춘 거냐”고 물었고, 그 질문 한 줄이 디버깅의 결을 완전히 바꿔놓았다.
그 뒤로는 데모가 안 붙을 때마다 코드를 보기 전에 peerConnection.iceConnectionState의 천이 로그부터 본다. checking → failed에서 멈췄다면 단계 4의 NAT 문제, connected 직후 검정 화면이라면 단계 6의 codec 문제, new에서 안 움직이면 단계 3의 STUN 문제. 이 한 가지 습관만 들여도 디버깅 시간이 눈에 띄게 짧아진다.
답글 남기기