블로그 목록
Media15분 읽기

WebRTC 연결 기초 — ICE, STUN, TURN, NAT

WebRTC의 media·data 연결과 signaling의 경계를 소개하고, NAT와 firewall 환경에서 ICE가 candidate pair를 검사하는 과정을 설명합니다. STUN으로 server-reflexive candidate를 얻고 TURN relay를 사용하는 경우를 구분하며, Agora Web SDK가 제공하는 channel·track API와 browser WebRTC 구현의 관계도 정리합니다.

WebRTCSTUNTURNICENAT

WebRTC(Web Real-Time Communication)는 플러그인이나 별도 소프트웨어 없이 브라우저 간에 오디오·영상·데이터를 실시간으로 주고받을 수 있게 하는 웹 표준입니다. 화상 통화, 화면 공유, P2P 파일 전송 등에 쓰입니다. Agora Web SDK처럼 브라우저에서 동작하는 관리형 RTC SDK는 이 브라우저 기능을 감싸 채널·토큰·미디어 트랙 API를 제공합니다. 이 글에서는 WebRTC와 P2P 연결에 함께 등장하는 NAT, STUN, TURN, ICE의 역할을 정리합니다.

WebRTC란?

MDN WebRTC API 문서에 따르면, WebRTC는 다음을 가능하게 합니다.

  • 미디어 캡처·스트리밍: 카메라, 마이크 등에서 오디오·영상을 가져와 브라우저 간에 전송
  • 임의 데이터 교환: 플러그인 없이 피어 간 직접 데이터 채널로 바이너리·텍스트 전송
  • 종단 간 통신: 중간 서버 없이 두 피어가 직접 연결되는 P2P 구조를 목표로 함

두 피어 간 연결은 RTCPeerConnection 인터페이스로 다루며, 연결이 수립되면 미디어 스트림(MediaStream)이나 데이터 채널(RTCDataChannel)을 붙여서 음성·영상·파일 등을 주고받습니다. 다만 실제 네트워크에는 NAT·방화벽이 있어서, “서로의 주소만 알면 바로 연결”이 되지 않는 경우가 많습니다. 이때 STUN, TURN, ICE가 필요합니다.

NAT와 P2P 연결의 한계

NAT는 내부망과 외부망 사이에서 사설 IP:포트와 공인 IP:포트를 매핑합니다. 외부에서는 “공인 IP:포트”로만 보이기 때문에, 상대방이 “내가 보이는 주소”를 모르면 P2P 연결을 시도하기 어렵습니다. 또한 NAT의 매핑·필터링 동작에 따라 외부에서 들어오는 패킷의 허용 범위가 달라져 P2P 연결에 제약이 생깁니다.

  • Full Cone NAT: 한 번 매핑되면 다른 쪽에서도 해당 포트로 접속 가능
  • Restricted Cone NAT: 클라이언트가 접속했던 서버(IP 기준)만 해당 포트로 접속 가능
  • Port Restricted Cone NAT: 접속했던 서버의 IP·포트만 접속 가능
  • Symmetric NAT: 목적지에 따라 매핑된 포트가 달라질 수 있어 직접 연결이 까다로움

이 네 가지는 RFC 3489의 레거시 분류입니다. 현행 NAT 동작은 RFC 4787처럼 매핑과 필터링을 분리해 설명하는 편이 정확합니다.

그래서 “내가 외부에 어떻게 보이는지 알아내는 것”과 “그 정보를 상대에게 전달해 서로 연결을 시도하는 것”을 표준화한 프로토콜이 STUN, TURN, ICE입니다.

STUN (Session Traversal Utilities for NAT)

STUN은 현행 RFC 8489에 정의된 프로토콜입니다. RFC 8489는 이전 규격인 RFC 5389를 대체합니다.

  1. STUN 서버가 관찰한 IP·포트 매핑을 응답으로 돌려줌
  2. ICE가 사용할 Server Reflexive Candidate를 수집하게 함

클라이언트가 STUN 서버에 요청을 보내면, 서버는 요청에서 관찰한 매핑 주소를 응답으로 돌려줍니다. ICE는 이 주소를 Server Reflexive Candidate로 상대에게 전달하고 직접 연결을 검사합니다. STUN 자체는 미디어를 중계하지 않습니다. TURN 사용 비율은 NAT·기업 방화벽·통신사망 정책에 따라 달라지므로 고정 비율을 일반화할 수 없으며, 운영 서비스의 ICE 후보 통계로 측정해야 합니다.

TURN (Traversal Using Relays around NAT)

TURN은 현행 RFC 8656에 정의된 릴레이 프로토콜입니다. RFC 8656은 RFC 5766을 대체합니다. 직접 후보 쌍의 연결 검사에 실패하면 TURN 서버가 중간에서 데이터를 받아 다른 피어에게 전달하는 relayed candidate를 사용할 수 있습니다.

  • 클라이언트는 TURN 서버에 Allocate Request를 보내고, 서버가 할당한 Relayed Transport Address를 받습니다.
  • 이 주소를 시그널링을 통해 상대에게 전달하면, 상대는 그 주소(TURN 서버)로 접속하고, TURN 서버가 해당 클라이언트와의 세션으로 패킷을 릴레이합니다.

TURN 경로에서는 실제 미디어가 서버를 경유하므로 서버 대역폭·비용이 듭니다. 대신 직접 후보 쌍이 연결되지 않는 환경에서 통신 경로를 제공합니다.

ICE (Interactive Connectivity Establishment)

ICE는 사용 가능한 모든 연결 후보(Candidate) 를 모아서, 그중에서 실제로 연결 가능한 경로를 선택하는 방식입니다. 후보는 대략 세 가지입니다.

  1. Host Candidate: 로컬 네트워크 인터페이스의 주소(같은 LAN 내 연결용)
  2. Server Reflexive Candidate: STUN을 통해 얻은 공인 IP:포트
  3. Relayed Candidate: TURN 서버가 할당한 릴레이 주소

ICE는 이 후보들을 상대에게 전달하고(SDP/시그널링), 후보 쌍마다 STUN 연결 검사를 수행합니다. 그런 다음 RFC 8445의 후보·후보 쌍 우선순위와 nomination 절차에 따라 사용할 쌍을 정합니다. 측정된 대역폭이나 장기 안정성을 비교해 경로를 최적화하는 알고리즘은 아닙니다. WebRTC에서는 이 과정이 RTCPeerConnection 설정 중 수행되며, 애플리케이션은 RTCConfiguration.iceServers에 STUN/TURN 서버를 지정합니다.

핵심 흐름 순서

                  Signaling Server (WebSocket/HTTP)
                  ┌──────────────────────────────┐
                  │  SDP(offer/answer) + ICE 후보  │
                  └────▲────────────────────▲─────┘
                       │                    │
        ┌──────────────┴──┐            ┌────┴──────────────┐
        │   Peer A         │            │   Peer B          │
        │ (NAT 뒤)         │            │ (NAT 뒤)          │
        └──┬────┬───────┬──┘            └──┬───────┬────┬───┘
           │    │       │                  │       │    │
   ┌───────┘    │       └──── ① STUN ──────┘       │    └───────┐
   │ Host       │ Srflx (공인 IP:포트)             │ Host       │
   ▼            ▼                                  ▼            ▼
 같은 LAN      ② ICE 후보 교환 → ③ 연결 체크 → 최적 경로 선택
   │                                                            │
   ├─ Host/Srflx 경로 성공 → P2P 직접 연결 (미디어 직통)        │
   └─ 직접 후보 쌍 연결 실패 → Relayed Candidate 사용           │
          └─ ④ Relayed 후보 → TURN 서버 경유 릴레이 (Fallback) ──┘
  1. ① STUN 요청 → 공인 IP:포트 획득
  2. ② SDP/ICE 후보 → Signaling Server 통해 교환
  3. ③ ICE 체크 → 최적 경로 선택
  4. P2P 직접 연결 또는 ④ TURN 릴레이 (Fallback) (클릭 시 연결 흐름 이미지)

WebRTC API 관점에서 정리

MDN WebRTC API 문서에서 강조하는 것처럼, WebRTC는 다음처럼 구성됩니다.

  • RTCPeerConnection: 피어 간 연결의 생성·관리
  • MediaStream / MediaStreamTrack: 오디오·영상 캡처 및 스트리밍
  • RTCDataChannel: 임의 데이터(파일, 채팅 등) 교환
  • RTCIceCandidate, RTCSessionDescription: ICE 후보와 SDP를 이용한 시그널링

시그널링(누가 누구에게 어떤 주소·코덱을 쓸지 협상)은 WebRTC 표준 밖에 있으므로, 웹소켓·HTTP 등으로 구현합니다. Agora Web SDK는 이 저수준 시그널링과 채널 연결을 애플리케이션 API 뒤로 감춥니다. Agora의 비공개 내부 경로 선택 구현을 표준 ICE 동작과 동일하다고 단정해서는 안 됩니다.

WebRTC API 요약 코드

아래는 위 네 가지를 브라우저 JavaScript로 다룰 때의 요약 예시입니다. 시그널링(offer/answer·ICE 교환)은 웹소켓 등 별도 채널로 전달한다고 가정합니다.

RTCPeerConnection: 피어 간 연결의 생성·관리

// STUN/TURN 서버 설정 → ICE 후보 수집·연결에 사용
const config = {
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    { urls: "turn:turn.example.com", username: "user", credential: "pass" },
  ],
};
// 피어 연결 객체 생성 (연결의 생성·관리)
const pc = new RTCPeerConnection(config);

// ICE 후보가 수집되면 시그널링 서버를 통해 상대에게 전달
pc.onicecandidate = (e) => {
  if (e.candidate) sendToPeer({ type: "ice", candidate: e.candidate });
};
// 연결 상태 변경 감지
pc.onconnectionstatechange = () => {
  console.log("연결 상태:", pc.connectionState); // new, connecting, connected, disconnected, failed, closed
};

MediaStream / MediaStreamTrack: 오디오·영상 캡처 및 스트리밍

// getUserMedia로 카메라·마이크 캡처 → MediaStream 획득
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// 스트림은 여러 트랙(MediaStreamTrack)으로 구성 (오디오·영상 캡처 및 스트리밍)
stream.getTracks().forEach((track) => {
  pc.addTrack(track, stream); // RTCPeerConnection에 트랙 추가 → 상대에게 전송
});

// 상대 피어에서 수신한 트랙 처리
pc.ontrack = (e) => {
  // e.streams[0] 또는 e.track을 재생용 요소(<video>, <audio>)에 연결
  document.querySelector("video#remote").srcObject = e.streams[0];
};

RTCDataChannel: 임의 데이터(파일, 채팅 등) 교환

// 데이터 채널 생성 (임의 데이터 교환: 파일, 채팅, 제어 메시지 등)
const dataChannel = pc.createDataChannel("chat", { ordered: true });

dataChannel.onmessage = (e) => {
  console.log("수신:", e.data); // 텍스트 또는 Blob 등
};
dataChannel.onopen = () => {
  dataChannel.send("안녕하세요"); // 연결 후 데이터 전송
};

// 상대가 만든 채널 수신 (setRemoteDescription 후 발생)
pc.ondatachannel = (e) => {
  const channel = e.channel;
  channel.onmessage = (ev) => console.log("상대 메시지:", ev.data);
};

RTCIceCandidate, RTCSessionDescription: ICE 후보와 SDP를 이용한 시그널링

// 1) Offer 생성 (발신 측)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// SDP(세션 설명)를 시그널링 서버로 전달 → RTCSessionDescription
sendToPeer({ type: "offer", sdp: pc.localDescription });

// 2) Answer 생성 (수신 측): 상대의 offer 수신 후
// setRemoteDescription(offer) 후 answer 생성
await pc.setRemoteDescription(new RTCSessionDescription(receivedOffer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeer({ type: "answer", sdp: pc.localDescription });

// 3) ICE 후보 교환: RTCIceCandidate를 시그널링으로 전달
// (onicecandidate에서 전송한 candidate를 수신 측에서)
await pc.addIceCandidate(new RTCIceCandidate(receivedCandidate));

위 순서(offer → answer → ICE 후보 교환)와 SDP·ICE 후보를 웹소켓 등으로 주고받는 부분이 시그널링이며, WebRTC 표준에는 포함되지 않습니다.

Agora Web SDK를 쓰면? 인프라는 SDK가 처리해 줍니다

네이티브 WebRTC로 화상 통화를 만들면 RTCPeerConnection 생성, offer/answer, ICE 후보 교환, 미디어 트랙 연결을 직접 구현해야 합니다. Agora Web SDK(JavaScript) 는 이 저수준 연결 절차를 join·publish·subscribe API로 추상화합니다. 다만 애플리케이션은 토큰을 백엔드에서 안전하게 발급하고, 권한·트랙·이벤트·재연결 UI를 관리해야 합니다.

Agora Web SDK로 화상 통화 시작하기 (간단 예시)

import AgoraRTC from "agora-rtc-sdk-ng";

// 1) 클라이언트 생성 (mode: "rtc" = 통화 모드)
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

// 2) 토큰은 백엔드에서 발급받음. ICE/STUN/TURN/시그널링은 SDK가 처리
const token = await fetch(`/api/token?channel=${channelName}&uid=${uid}`).then((r) => r.json()).then((d) => d.token);

// 3) 채널 입장
await client.join(APP_ID, channelName, token, uid);

// 4) 마이크·카메라 트랙 생성 후 퍼블리시 (한 번에 처리)
const [audioTrack, videoTrack] = await Promise.all([
  AgoraRTC.createMicrophoneAudioTrack(),
  AgoraRTC.createCameraVideoTrack(),
]);
await client.publish([audioTrack, videoTrack]);

// 5) 원격 사용자 영상 수신 시 구독 후 재생
client.on("user-published", async (user, mediaType) => {
  await client.subscribe(user, mediaType);
  if (mediaType === "video") user.videoTrack?.play(document.getElementById("remote-video"));
  if (mediaType === "audio") user.audioTrack?.play();
});
  • RTCPeerConnection, offer/answer, ICE 후보 교환, addTrack/ontrack 같은 코드를 직접 쓸 필요가 없습니다.
  • 채널 토큰만 백엔드에서 받아서 join에 넘기면, 나머지 연결·경로 선택은 Agora Web SDK와 서버가 알아서 처리합니다.

실제 동작하는 데모는 RTC 데모에서, 구현 단계는 Agora RTC를 활용한 실시간 화상 통화 구현하기에서 자세히 다룹니다.

실제 Agora RTC를 이용한 화상 통화 구현은 Agora RTC를 활용한 실시간 화상 통화 구현하기 글을 참고하면 됩니다.

관련 글

참고 자료

실제 데모

/agora-demo/rtc

© 2026 Frank Kim. All rights reserved.