본문으로 건너뛰기
Previous
Next
[2026] WebRTC 완전 참조 — PeerConnection·ICE·STUN·TURN·시그널링·SFU

[2026] WebRTC 완전 참조 — PeerConnection·ICE·STUN·TURN·시그널링·SFU

[2026] WebRTC 완전 참조 — PeerConnection·ICE·STUN·TURN·시그널링·SFU

이 글의 핵심

WebRTC는 표준 API와 ICE·DTLS-SRTP를 통해 브라우저와 네이티브 클라이언트 간 **저지연** 미디어·데이터 경로를 만듭니다. 이 글은 W3C [WebRTC(webrtc-pc)](https://www.w3.org/TR/webrtc/) 스펙, SDP/ICE 트랜잭션, STUN·TURN, 시그널링·Trickle ICE, Perfect Negotiation, 1:1/멀티파티(SFU), 화면 공유, Simulcast·SVC, 대역·혼잡 제어, 운영(코턴·배터리·스케일)을 한 권에 모은 **엔지니어링 참고서**입니다.

Web Real-Time Communication(WebRTC)는 W3C Identifiers for WebRTC’s Statistics API·getUserMedia·WebRTC: Real-Time Communication in Browsers(흔히 webrtc-pc로 부름)에 정의된 브라우저 API와, IETF가 표준화한 미디어·전송 프로토콜(ICE, DTLS, SRTP, SCTP 등)의 합집합으로 이해하는 것이 실무에 유리합니다. HLS·DASH가 HTTP 단방향(대개)에 특화되었다면, WebRTC는 쌍방향 RtpTransport에 특화됩니다. 이 문서는 스펙 → 시그널링 → 1:1 구현 → 인프라 → 고급 코덱/전송 → 운영 순으로 정리하며, 링크는 안정한 W3C TR(권고안)·RFC·대표 구현·오픈소스 SFU를 함께 둡니다.

읽는 범위: W3C webrtcRTCPeerConnection·RTCDataChannel·이벤트·통계, mediacapture-streamsgetUserMedia·getDisplayMedia, 그리고 ICE(RFC 8445)·STUN(RFC 5389)·TURN(RFC 8656)를 기준으로 하되, 브랜드별(크롬·사파리·FF) sdpSemantics·코덱·H.264/AV1 정책은 캡빌로 검증하십시오.

flowchart LR
  subgraph A[Client A]
    PC1[RTCPeerConnection]
    M1[MediaStream]
  end
  subgraph S[Signaling (out-of-band)]
    WSS[WebSocket / App Server]
  end
  subgraph B[Client B]
    PC2[RTCPeerConnection]
    M2[MediaStream]
  end
  STUN[STUN]
  TURN[TURN]
  PC1 <--> WSS
  WSS <--> PC2
  PC1 -.- STUN
  PC2 -.- STUN
  PC1 -.- TURN
  PC2 -.- TURN
  M1 -.-|SRTP/ICE| M2

1. 프로토콜 스펙

1.1 W3C WebRTC 아키텍처

  • MediaStream / MediaStreamTrack: mediacapture-streams에 정의된 로컬·원격 오디오/비디오(및 getDisplayMedia로 캡처한 화면) 트랙 컨테이너. 트랙은 RTCRtpSender·RTCRtpReceiver와 연결됩니다.
  • RTCPeerConnection: RTCPeerConnectionICE 후보 수집, DTLS 핸드셰이크, SRTP 키 유도, RTP/RTCP 송수신을 총괄합니다. getStats()로 RTT·손실·지터를 관측합니다( webrtc-stats ).
  • RTCDataChannel: RTCDataChannel은 SCTP over DTLS(구현·프로파일에 따라)로 일반 이진/텍스트 데이터를 전달하며, 시그널 서버를 거치지 않고도 낮은 지연·순서·신뢰성 옵션(부분/전체)을 줄 수 있습니다. 시그널과 혼동하지 마십시오 — 세션 협상은 여전히 별도 채널이 필요할 수 있습니다.
// DataChannel: Offer/Answer와 별도로 생성·협상되며, 시그널 경로가 아닌 P2P 데이터 경로를 씀
const dc = pc.createDataChannel("app", { ordered: true, maxRetransmits: 3 });
dc.onopen = () => dc.send("hello");
dc.onmessage = (e) => console.log(e.data);
// 수신 측
pc.ondatachannel = (ev) => {
  const ch = ev.channel;
  ch.onmessage = (e) => { /* ... */ };
};

실무: RTCPeerConnectionSTUN·TURN(ICE servers)·RTCRtpTransceiver 정책(수신전용, sendrecv)·코덱 선호도를 setConfiguration·addTransceiver·addTrack에 반영해 설계합니다.

1.2 SDP: Offer / Answer (JSEP)

W3C는 JSEP (JavaScript Session Establishment Protocol) 관점에서 브라우저가 SDP 문자열로 세션을 서술합니다. 흐름은 일반적으로 다음과 같습니다.

  1. OffercreateOffer → (필요 시) setLocalDescription(offer)
  2. 상대 Answer — 원격 setRemoteDescription(offer)createAnswersetLocalDescription(answer)
  3. ICE candidateonicecandidate / icecandidate 이벤트(트릴드 ICE) 또는 end-of-candidates 처리

IETF RFC 8829 (JSEP)과 브라우저 프로파일이 상세하며, BUNDLE RFC 9143로 오디오·비디오·데이터를 단일 5-튜플에 묶는 것이 흔합니다. Plan B vs Unified Plan: 현행 브라우저는 Unified Plan(rtpParameters)에 맞춰 addTransceiver를 쓰는 쪽이 유지보수에 유리합니다.

1.3 ICE: Interactive Connectivity Establishment

  • ICE(RFC 8445): RFC 8445는 후보(호스트·서버 리플렉시브·릴레이)를 수집·우선순위화·체크하여 P2P 가능 경로를 찾습니다.
  • STUN(RFC 5389): RFC 5389 — NAT 매핑 조회(공인 주소·포트 후보), 주로 서버 반사 후보.
  • TURN(RFC 8656): RFC 8656미디어 릴레이 후보(대칭 NAT 등에서 P2P 실패 시). 운영 비용·지연·로그가 늘어납니다.
  • Trickle ICE: RFC 8838 — 후보를 점진적으로 교환(완성된 SDP에 모두 박는 방식과 혼용 금지 시 주의).

RTCPeerConnection 구성에 iceServers: [{ urls: "stun:…" }, { urls: "turn:…", username, credential }]를 전달하며, TURNturns:(TLS)로 암호화 클라이언트-서버 구간을 보강하는 경우가 많습니다.

1.4 DTLS-SRTP: 암호화

  • DTLS(UDP): RFC 6347RTCPeerConnection는 ICE 후보 쌍이 확정되면 DTLS로 키를 협상합니다.
  • SRTP / SRTCP: RFC 3711 — 페이로드·RTCP 기밀성·무결성.
  • DTLS-SRTP 프로파일: RFC 5764 — DTLS로 SRTP 마스터 키/솔트를 유도. WebRTC “미디어” 경로는 엔드-투-엔드가 아닌 ‘전송 암호화’ — 중간 SFU가 미디어를 볼 수 있고, 진짜 E2EE는 Insertable Streams·별도 E2E 프레임워크(조직·호환·키 관리 복잡)를 논의합니다.

2. 시그널링 (WebSocket)

WebRTC는 시그널 프로토콜을 스펙에 넣지 않습니다. 대신 임의 신뢰 채널로 SDP와 ICE JSON을 전달합니다. W3C § 4.3.1 주변 RTCPeerConnection 흐름을 코드와 맞춥니다.

2.1 WebSocket을 통한 메시지 모델

  • 최소 페이로드: { type, payload } where typejoin | offer | answer | ice-candidate | bye
  • ID·룸: 복수 참가자면 roomId, from, to (또는 SFU로 위임) 스키마를 고정
  • 보안: WSS + 인증(쿠키/토큰) + 룸 권한 — 공개 STUN/만능 데모는 시그 휘발·도청 위험

2.2 SDP / ICE candidate 교환

  • Offer/Answer는 한 번씩 아니라 재협상(Renegotiation) 시 여러 번 발생할 수 있음(화면 공유·트랙 추가).
  • addIceCandidate(null)(또는 end of candidates 규칙)으로 트릭 완료를 맞출 것.

2.3 Trickle ICE

RTCIceTransport / onicecandidate후보마다 전송 — 초기 연결이 빨라질 수 있음. 서버는 순서 보장만 하면 됩니다(ICE는 out-of-order에 견딤).

2.4 Perfect Negotiation (권장 패턴)

둘 다 동시에 Offer를 만들면 글레어링(glare)가 납니다. W3C RTCPeerConnection의 협상 상태(signalingState)와 함께 쓰는 Perfect negotiation 커뮤니티 패턴( polite / impolite + onnegotiationneeded + 롤백)을 쓰면 단일 코드 경로로 안정적입니다. 핵심은 (1) impolite 쪽이 충돌 시 incoming offer에 응답하고, (2) polite 쪽이 자신의 진행 중 offer를 롤백할 수 있게 하는 점입니다.

// Perfect Negotiation 스케치 ( polite / impolite 는 룸·역할에 따라 1:1로 고정 )
function makePerfectNegotiation(pc, signalingSend, { polite }) {
  const queue = new Map(); // mid → candidate until remoteDescription
  const ice = (e) => {
    if (e.candidate) signalingSend({ type: "ice", candidate: e.candidate });
  };
  let makingOffer = false;
  pc.onnegotiationneeded = async () => {
    try {
      makingOffer = true;
      await pc.setLocalDescription(await pc.createOffer());
      signalingSend({ type: "offer", sdp: pc.localDescription });
    } catch (e) { console.error(e); }
    finally { makingOffer = false; }
  };
  pc.onicecandidate = ice;
  return {
    async onMessage(msg) {
      if (msg.type === "offer") {
        if (pc.signalingState !== "stable" && !polite) return; // impolite: ignore glare
        if (pc.signalingState === "stable" && (makingOffer || pc.remoteDescription)) {
          if (!polite) return;
          await pc.setLocalDescription({ type: "rollback" });
        }
        await pc.setRemoteDescription(msg);
        if (pc.signalingState === "have-remote-offer")
          await pc.setLocalDescription(await pc.createAnswer());
        signalingSend({ type: "answer", sdp: pc.localDescription });
      } else if (msg.type === "answer") {
        await pc.setRemoteDescription(msg);
      } else if (msg.type === "ice" && msg.candidate) {
        try { await pc.addIceCandidate(msg.candidate); } catch (e) { /* stale */ }
      }
    }
  };
}

상단 스케치는 이해용이며, 실서비스에서는 rollback 지원, 큐, 타임스탬프, 구버전 폴리필을 적용하십시오. Mozilla WebRTC Blog — Perfect negotiation in WebRTC의 그림·순서도를 함께 보면 glare 처리 흐름이 분명해집니다.


3. 실전 구현

3.1 브라우저(1:1) — getUserMedia + WebSocket

아래는 WSSoffer/answer/ice를 주고받는 1:1 흐름입니다. 피어는 서버가 role: "caller"로, 둘째callee로 내려주는 역할 고정 패턴이며, 양쪽이 동시에 createOffer를 부르지 않게 합니다(프로덕션에서는 async 에러, 재시도, TURN, 권한 UI, Perfect Negotiation·재협상을 추가).

<!-- index.html: 로컬·원격 <video> + ?room= + 서버가 caller/callee 를 지정 -->
<script type="module">
  const room = new URLSearchParams(location.search).get("room") || "demo";
  const base = (location.protocol === "https:" ? "wss://" : "ws://") + location.host;
  const ws = new WebSocket(base);
  const pc = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  });
  const localVideo = document.getElementById("local");
  const remoteVideo = document.getElementById("remote");
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
  localVideo.srcObject = stream;
  stream.getTracks().forEach((t) => pc.addTrack(t, stream));
  pc.ontrack = (ev) => {
    if (!remoteVideo.srcObject) remoteVideo.srcObject = new MediaStream();
    remoteVideo.srcObject.addTrack(ev.track);
  };
  pc.onicecandidate = (e) => {
    if (e.candidate) ws.send(JSON.stringify({ t: "ice", c: e.candidate.toJSON(), room }));
  };
  let role = "waiting";
  ws.onopen = () => ws.send(JSON.stringify({ t: "join", room }));
  async function asCaller() {
    await pc.setLocalDescription(await pc.createOffer());
    ws.send(JSON.stringify({ t: "offer", sdp: pc.localDescription, room }));
  }
  ws.onmessage = async (ev) => {
    const m = JSON.parse(ev.data);
    if (m.t === "join-ok" && m.room === room) {
      role = m.role; // "caller" | "callee"
      if (role === "caller") await asCaller();
    }
    if (m.t === "offer" && m.room === room && role === "callee") {
      await pc.setRemoteDescription(m.sdp);
      await pc.setLocalDescription(await pc.createAnswer());
      ws.send(JSON.stringify({ t: "answer", sdp: pc.localDescription, room }));
    }
    if (m.t === "answer" && m.room === room && role === "caller")
      await pc.setRemoteDescription(m.sdp);
    if (m.t === "ice" && m.room === room && m.c) {
      try { await pc.addIceCandidate(m.c); } catch { /* 만료/순서 */ }
    }
  };
</script>

의미: addTrack은 Unified Plan에서 RTCRtpTransceiver를 붙이고, caller만 첫 createOffer를 보냅니다. 트랙 추가negotiationneeded재협상을 요구할 때는 §2.4 Perfect Negotiation 패턴이나, “협상 진행 중” 뮤텍스로 Offer·Answer 충돌을 막는 규율을 둡니다. TURN이 없는 환경에서는 iceConnectionState === "failed"·"disconnected"가 늘어납니다.

3.2 Node.js 시그널 서버 (ws)

// server.mjs — npm i ws
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
const rooms = new Map();
function roomSet(r) {
  if (!rooms.has(r)) rooms.set(r, new Set());
  return rooms.get(r);
}
wss.on("connection", (ws) => {
  let room;
  ws.on("message", (raw) => {
    const m = JSON.parse(raw.toString());
    if (m.t === "join" && m.room) {
      room = m.room;
      const set = roomSet(room);
      const first = set.size === 0;
      set.add(ws);
      ws.send(JSON.stringify({ t: "join-ok", room, role: first ? "caller" : "callee" }));
      return;
    }
    if (!room) return;
    for (const p of roomSet(room)) if (p !== ws) p.send(raw.toString());
  });
  ws.on("close", () => {
    if (room && rooms.has(room)) {
      rooms.get(room).delete(ws);
      if (rooms.get(room).size === 0) rooms.delete(room);
    }
  });
});
console.log("WS signaling on 8080");

의미: join-okcaller/callee를 확정한 뒤, 이후 offer/answer/ice다른 클라이언트에 브로드캐스트합니다(인증·스팸 방지·SFU 라우팅 없음). SFU(§3.4)는 서버에 RTP종단하는 프로세스가 있어야 합니다.

3.3 STUN / TURN — Coturn 예시

Coturn turnserver.conf 샘플(도메인·시크릿·TLS 인증서 경로는 환경에 맞게):

# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
realm=turn.example.com
fingerprint
use-auth-secret
static-auth-secret=REPLACE_WITH_LONG_RANDOM
total-quota=100
bps-capacity=0
log-file=/var/log/turn.log
# 외부 IP (퍼블릭 NIC)
external-ip=203.0.113.5
# (선택) TURN over TLS: cert/pkey
# cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
# pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
userdb=/etc/turnuserdb.conf
no-loopback-peers
no-multicast-peers

클라이언트(REST) 자격: 많은 앱이 HMAC(만료 time-window) TURN 임시 크레덴셜을 twilio·xirsys·자체 엔드포인트로 발급합니다. 장기 고정 username:password유출 위험.

3.4 멀티파티: Mesh / SFU / MCU

토폴로지송신 업링드수신 다운링크(각 피어)서버 CPU쓰는 경우
Mesh(풀 P2P)(N-1)개 스트림 송신(N-1)개 수신서버: 시그만2~4인 화면일 때
SFU1 upstream선택적 n개(뷰)중~대(포워딩·BWE)대부분 회의·라이브
MCU1(보통)1 합성높음(디코딩/합성)구형 H.323·방송 합성·전화망

SFURTP/RTCP그대로 포워딩하며(트랜스코딩 없이), SVC·Simulcast·BWE·NACK/PLI·REMB/Transport-CC끊김 없이 연동됩니다.

3.5 화면 공유: getDisplayMedia

Screen CapturegetDisplayMedia({ video: { cursor: "always" }, audio: true })새 트랙addTrack / addTransceiver하거나, replaceTrack으로 비디오만 갈아끼웁니다. 사용자가 중지하면 track.onended에서 협상/뷰 정리. 권한·프라이버시(사용자 알림 영역): 브랜딩/운영자 가이드라인에 맞는 UX가 필요합니다.

async function shareScreen(pc, sender) {
  const s = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
  const v = s.getVideoTracks()[0];
  if (sender) await sender.replaceTrack(v);
  else { s.getTracks().forEach((t) => pc.addTrack(t, s)); }
  v.onended = () => s.getTracks().forEach((t) => t.stop());
}

4. 고급 주제: 코덱·전송

4.1 Simulcast (Sender)

W3C addTransceiver + setParameters / encodings: [{ rid: "h", maxBitrate, scaleResolutionDownBy }, …]하나MediaStreamTrack다중 RTP 스트림(레이어)을 씁니다. 수신/중계RID/RRID·BWE에 따라 한 레이어를 고릅니다. Open Source SFU(§5)는 대부분 Simulcast 라우팅을 정식 지원합니다.

const tx = pc.addTransceiver("video", {
  sendEncodings: [
    { rid: "f", maxBitrate: 1_200_000 },
    { rid: "h", maxBitrate: 500_000, scaleResolutionDownBy: 2 },
    { rid: "q", maxBitrate: 150_000, scaleResolutionDownBy: 4 },
  ],
  direction: "sendrecv",
});
// 이후 getUserMedia VideoTrack 을 addTrack

4.2 SVC (Scalable Video Coding)

AV1, VP9, H.264 SVC 등 — 하나비트스트림temporal / spatial 계층. SFU는 RTP 헤더 확장(Dependency Descriptor 등)에 따라 SVC 레이어를 선택합니다(구현·브라우저·코덱 협상 sdpFmtpLine 필수). Simulcast vs SVC: 대역한계가 큰 모바일 업링드에는 SVC가 유리한 경우가 있고, 미디어 서버가 SVC 라우터로 동작해야 합니다.

4.3 Bandwidth Estimation (BWE) & Congestion Control

  • REMB(RFC 5104) / TMMBR/VP8 (레거시) / Google CC (GCC) / Transport-wide CC (transport-cc) RFC 8888 (RTCP feedback) / NADA (일부) — 실제 알고리즘은 브랜드·빌드·RTCConfiguration에 따라 이질적.
  • getStats()candidate-pair·inbound-rtp·outbound-rtp·remote-inbound-rtp에서 손실·RTT를 보고, 팀은 앱 수준 비트레이트 캡·해상도 동적 조정(카메라 캡처)을 병행합니다.
  • TWCC(전송): draft-holmer-rmcat-transport-wide-cc-extensions(IETF 개념) — 송신이 패킷 단위 피드백을 보면 비트레이트올리고 내리는 데 쓰입니다(구체 파라미터는 구현이 다름).

4.4 Forward Error Correction (FEC)

  • NACK/RTX: 재전송(지연·버퍼와 트레이드오프). ulpfec / flexfec / red — 오디오(RED)는 낮은 지연, 비디오는 유연 FEC(조직/코덱에 따라)가 상이.
  • setCodecPreferences (가능한 브라우저) / SDP offer 단계에서 FEC 관련 payload type·fmtp을 고정. 실무특정 네트워크(위성, LTE)에서 NACK+FEC 조합을 A/B.

5. 최적화

5.1 비트레이트 적응

  • RTCRtpSender getParameters / setParameters encodings maxBitrate, maxFramerate, scaleResolutionDownBy동적 조정(저전력/저대역).
  • contentHint ( "motion" | "detail" | "text" ) — 캡처·인코딩 의도 힌트(지원 varies).

5.2 네트워크 QoS (DSCP)

RTCRtpSenderdscp / 전송 우선도(브랜드 제한) — 와이파이/모바일이 DSCP을 무시하거나 리맵하는 경우가 많아 E2E 보장은 기대하기 어렵습니다. TURN 위치·CDN/리전·BWE(§4.3)이 체감 품질에 더 크게 작용하는 경우가 많습니다.

5.3 CPU 사용량

  • 해상도·FPS·Simulcast 레이어 수 제한, SVC 1~2 temporal·뷰 4분할 제한.
  • 화면 공유: 초당 키프레임/전체 프레임 CPU 부하가 큼 — 콘텐츠 변화가 적을 때 프레임 스로틀(고급) 또는 정적 슬라이드·문서 캡처 모드를 검토합니다.
  • worker + WASM은 필터·E2E·ML 쪽(전송 자체는 C++ 브라우저 쪽이 큼).

5.4 모바일 배터리

  • 백그라운드 replaceTrack 카메라 끄기 / 듣기전용 (recvonly) 전환(협상).
  • 오프스크린 탭/앱: OS가 ICE / RTCRtp스로틀 수 있음 — PWA 백그라운드 정책을 확인.
  • TURN 대신 P2P 우선은 배터리가 아니라 대역 쪽 이득이 큼(실측 권).

5.5 대규모 스케일링: Janus, Jitsi, mediasoup

구성요소설명적합
mediasoupNode/Rust/어느 언어 Worker; 최적 SFU 제어(라우터·Consumer·Producer)커스텀 제품, 높은 동시
Janus플러그인 아키텍처; SFU·MCU·SIP 브릿지텔레콤·녹화·게이트웨이
Jitsi (Jitsi Videobridge)오픈 스택(웹+모바일), 대규모 커뮤니티빠른 자체 호스팅 회의
LiveKit / PionGo 기반, 클라우드 관리형도 존재인프라규모·SLA

운영: 수평 미디어 노드( SFU pod ) + 시그/룸 상태 Redis + *TURN 클러스터 + 관측(Prometheus, getStats 집계). 전송 지연 RTT 분포 SLI.


6. 점검 체크리스트 (요약)

  1. webrtc-pcRTCPeerConnection 상태 머신( signaling / ice / connection )을 로그.
  2. TURN 가용(지역, TLS 5349), 자격 회전.
  3. Perfect negotiation (동시 offer) 방지.
  4. getStats패킷 손실 / RTT / (SFU) producer 유효 비트.
  5. Simulcast / SVC 호환원격 라우터 버전.
  6. 개인정보로그SDP / 캡처 썸네일 금지.

결론

W3C WebRTC API는 ICEDTLS-SRTP와 맞물릴 때 브라우저 기반 실시간 통신의 사실상 표준이 됩니다. 시그널링은 WebSocket 외에도 SSE·HTTP·전용 메시징 등 어떤 신뢰 채널이든 쓸 수 있으나, WSS + 인가 + 룸 권한이 보안의 최소 선입니다. 미디어 경로는 STUN만으로는 부족한 경우가 많아 Coturn 수준의 TURNSLO(가용·지연)에 포함하는 편이 안전합니다. Simulcast·SVC·BWE뷰 수업링드 한계를 함께 다루는 대표 수단이며, mediasoup·Janus·Jitsi는 팀 역량·SLA·PSTN/SIP 연동 요구에 맞춰 SFU 한 계층을 고정·확장하는 데 흔히 쓰입니다.

참고(표준·문서): W3C WebRTC 1.0 · W3C Media Capture and Streams · W3C Identifiers for WebRTC’s Statistics · IETF RFC 8445 (ICE) · RFC 5389 (STUN) · RFC 8656 (TURN) · RFC 8829 (JSEP) · RFC 5764 (DTLS-SRTP).

배포 전 git add·commit·git pushnpm run deploy(CLAUDE.md)를 권장합니다.