이미지 로딩 중...

ICE Candidate 처리 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 3 Views

ICE Candidate 처리 완벽 가이드

WebRTC 연결을 위한 ICE Candidate 처리 과정을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. onicecandidate 이벤트부터 addIceCandidate 메서드, Trickle ICE 구현, NAT 트래버설까지 실무에서 바로 적용 가능한 예제와 함께 안내합니다.


목차

  1. ICE 프로토콜 이해
  2. onicecandidate 이벤트
  3. addIceCandidate 메서드
  4. Trickle ICE 구현
  5. ICE 연결 상태 확인
  6. NAT 트래버설

1. ICE 프로토콜 이해

시작하며

여러분이 화상 통화 앱을 만들려고 할 때 이런 상황을 겪어본 적 있나요? 같은 네트워크에서는 잘 연결되던 영상 통화가 다른 네트워크에 있는 친구와는 전혀 연결되지 않는 경우 말이죠.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 사용자들은 각자 다른 공유기(라우터) 뒤에 있고, 서로의 정확한 위치(IP 주소)를 알 수 없기 때문입니다.

마치 큰 아파트 단지에서 같은 동에 사는 사람은 쉽게 찾지만, 다른 아파트 단지에 사는 친구를 찾으려면 정확한 주소가 필요한 것과 같습니다. 바로 이럴 때 필요한 것이 ICE(Interactive Connectivity Establishment) 프로토콜입니다.

ICE는 서로 다른 네트워크에 있는 두 사용자가 직접 연결될 수 있도록 여러 경로를 찾아주는 똑똑한 우체부 같은 역할을 합니다.

개요

간단히 말해서, ICE 프로토콜은 두 컴퓨터가 인터넷을 통해 직접 연결되기 위한 최적의 경로를 찾아주는 기술입니다. WebRTC로 실시간 통신을 구현할 때, 사용자들은 대부분 공유기나 방화벽 뒤에 있습니다.

이를 NAT(Network Address Translation) 환경이라고 하는데, 이런 환경에서는 외부에서 내부 기기로 직접 접근할 수 없습니다. ICE는 이런 장벽을 뚫고 연결할 수 있는 여러 방법을 시도합니다.

기존에는 중간 서버를 통해서만 통신했다면, ICE를 사용하면 가능한 경우 사용자끼리 직접 연결(P2P)하여 지연 시간을 줄이고 서버 비용도 절약할 수 있습니다. ICE의 핵심 특징은 크게 세 가지입니다.

첫째, 여러 네트워크 경로를 동시에 탐색합니다. 둘째, 가장 빠르고 안정적인 경로를 자동으로 선택합니다.

셋째, 연결이 실패하면 대체 경로로 자동 전환합니다. 이러한 특징들이 실시간 통신의 안정성과 품질을 크게 향상시킵니다.

코드 예제

// RTCPeerConnection 생성 시 ICE 서버 설정
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }, // STUN 서버로 공인 IP 확인
    {
      urls: 'turn:turn.example.com:3478', // TURN 서버로 릴레이 연결
      username: 'user',
      credential: 'pass123'
    }
  ],
  iceCandidatePoolSize: 10 // 미리 후보를 수집하여 연결 속도 향상
};

const peerConnection = new RTCPeerConnection(configuration);

설명

이것이 하는 일: ICE 프로토콜은 WebRTC 연결을 위한 네트워크 경로를 탐색하고 선택하는 전체 과정을 관리합니다. 첫 번째로, configuration 객체의 iceServers 배열은 네트워크 경로를 찾는 데 도움을 줄 서버들을 지정합니다.

STUN 서버는 여러분의 공인 IP 주소를 알려주는 역할을 하고, TURN 서버는 직접 연결이 불가능할 때 중계 역할을 합니다. 이렇게 설정하는 이유는 다양한 네트워크 환경에서도 연결이 가능하도록 여러 옵션을 준비하기 위함입니다.

그 다음으로, RTCPeerConnection 객체가 생성되면서 내부적으로 ICE 에이전트가 시작됩니다. 이 에이전트는 여러분의 컴퓨터가 가진 모든 네트워크 인터페이스(Wi-Fi, 유선, VPN 등)를 검사하고, 각각에 대해 가능한 연결 경로를 찾기 시작합니다.

iceCandidatePoolSize 옵션은 연결 시도 전에 미리 ICE candidate를 수집해 놓는 개수를 지정합니다. 이 값을 10으로 설정하면 연결 속도가 빨라지지만, 그만큼 초기 리소스를 더 사용합니다.

실제로 화상 회의 같은 빠른 연결이 중요한 서비스에서는 이 값을 높게 설정하는 것이 좋습니다. 여러분이 이 코드를 사용하면 WebRTC 연결의 성공률이 크게 향상됩니다.

특히 모바일 네트워크, 기업 방화벽, 공유 Wi-Fi 등 다양한 네트워크 환경에서도 안정적으로 연결할 수 있고, 연결 실패 시 자동으로 대체 경로를 시도하여 사용자 경험을 개선할 수 있습니다.

실전 팁

💡 무료 STUN 서버는 Google이 제공하는 것이 가장 안정적이지만, 프로덕션 환경에서는 자체 STUN/TURN 서버를 운영하는 것이 좋습니다. coturn 같은 오픈소스를 활용하면 비용을 절감할 수 있습니다.

💡 TURN 서버는 대역폭을 많이 사용하므로 비용이 발생합니다. iceTransportPolicy를 'relay'로 강제하지 말고 기본값인 'all'을 사용하여 가능한 경우 직접 연결을 우선 시도하세요.

💡 iceCandidatePoolSize를 너무 크게 설정하면 초기 연결 시 배터리와 데이터를 낭비할 수 있습니다. 모바일 앱에서는 5-10 정도가 적당합니다.

💡 여러 ICE 서버를 설정할 때는 지리적으로 가까운 서버를 우선 배치하세요. 한국 사용자가 많다면 국내 TURN 서버를 먼저 시도하도록 배열 순서를 조정하면 연결 속도가 빨라집니다.

💡 연결 문제를 디버깅할 때는 chrome://webrtc-internals 페이지를 활용하세요. ICE candidate 수집 과정과 연결 시도를 실시간으로 확인할 수 있어 문제 해결이 훨씬 쉬워집니다.


2. onicecandidate 이벤트

시작하며

여러분이 WebRTC 연결을 시도할 때 이런 궁금증을 가져본 적 있나요? "연결 정보가 생성되면 상대방에게 어떻게 전달하지?" 하는 의문 말이죠.

이 문제는 WebRTC 개발의 핵심입니다. 연결을 위한 네트워크 정보(ICE Candidate)가 생성되어도, 이를 상대방에게 전달하지 않으면 연결이 완성될 수 없기 때문입니다.

마치 여러분의 전화번호를 알아도 친구에게 알려주지 않으면 전화를 받을 수 없는 것과 같습니다. 바로 이럴 때 사용하는 것이 onicecandidate 이벤트입니다.

이 이벤트는 새로운 연결 경로가 발견될 때마다 자동으로 발생하여, 여러분이 그 정보를 상대방에게 보낼 수 있도록 알려줍니다.

개요

간단히 말해서, onicecandidate 이벤트는 로컬 컴퓨터에서 새로운 네트워크 연결 경로를 찾았을 때 발생하는 이벤트입니다. WebRTC 연결 과정에서 ICE 에이전트는 계속해서 새로운 연결 가능성을 탐색합니다.

Wi-Fi 주소, 유선 랜 주소, 공인 IP 주소 등 여러 경로를 찾을 때마다 이 이벤트가 발생합니다. 각 candidate는 "이 주소로도 나에게 연결할 수 있어!"라는 정보를 담고 있습니다.

기존에는 모든 candidate를 수집한 후 한 번에 전달했다면, 현대적인 WebRTC에서는 Trickle ICE를 사용하여 찾는 즉시 하나씩 전달합니다. 이렇게 하면 연결 속도가 크게 빨라집니다.

onicecandidate의 핵심 특징은 첫째, 비동기적으로 여러 번 발생한다는 점입니다. 둘째, 각 candidate는 우선순위를 가지고 있어 더 좋은 경로가 먼저 시도됩니다.

셋째, 모든 candidate 수집이 끝나면 null candidate가 전달됩니다. 이러한 특징들을 이해하면 효율적인 시그널링을 구현할 수 있습니다.

코드 예제

// ICE candidate 이벤트 리스너 등록
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    // 새로운 candidate를 찾았을 때
    console.log('새 ICE Candidate:', event.candidate.candidate);

    // 시그널링 서버를 통해 상대방에게 전송
    signalingServer.send({
      type: 'ice-candidate',
      candidate: event.candidate.toJSON()
    });
  } else {
    // candidate가 null이면 수집 완료
    console.log('ICE Candidate 수집 완료');
  }
};

설명

이것이 하는 일: onicecandidate 이벤트 핸들러는 로컬에서 발견된 연결 경로 정보를 받아서 상대방에게 전달하는 역할을 합니다. 첫 번째로, event.candidate를 확인하여 실제 candidate가 있는지 검사합니다.

candidate가 존재한다는 것은 새로운 연결 경로를 찾았다는 의미입니다. 여기서 event.candidate.candidate 속성은 SDP 형식의 문자열로, "candidate:1234 1 udp 2130706431 192.168.1.100 54321 typ host" 같은 형태를 가집니다.

이 문자열에는 IP 주소, 포트, 프로토콜, 우선순위 등 연결에 필요한 모든 정보가 담겨 있습니다. 그 다음으로, toJSON() 메서드를 사용하여 candidate 객체를 직렬화합니다.

이렇게 하는 이유는 네트워크를 통해 전송하려면 JavaScript 객체를 JSON 문자열로 변환해야 하기 때문입니다. signalingServer.send()는 WebSocket이나 Socket.io 같은 시그널링 채널을 통해 상대방에게 이 정보를 전달합니다.

마지막 else 블록은 매우 중요합니다. event.candidate가 null일 때는 ICE 에이전트가 모든 가능한 경로 탐색을 완료했다는 신호입니다.

이 시점을 알면 "연결 시도 중" 같은 UI 상태를 업데이트하거나, 타임아웃 타이머를 시작하는 등의 추가 로직을 구현할 수 있습니다. 여러분이 이 코드를 사용하면 실시간으로 candidate를 전달하여 연결 시간을 최소화할 수 있습니다.

또한 각 candidate의 정보를 로깅하여 연결 문제를 디버깅할 수 있고, 연결 과정의 진행 상황을 사용자에게 보여줄 수도 있습니다.

실전 팁

💡 event.candidate가 null인지 항상 확인하세요. null을 그대로 전송하면 상대방에서 에러가 발생할 수 있습니다. null은 "수집 완료" 신호로만 활용하세요.

💡 시그널링 서버 전송 시 try-catch로 에러를 처리하세요. 네트워크 문제로 candidate 전송이 실패하면 연결이 실패하거나 지연될 수 있으므로, 재시도 로직을 구현하는 것이 좋습니다.

💡 보안을 위해 candidate 정보를 로그에 남길 때는 주의하세요. candidate에는 내부 IP 주소가 포함되어 있어 프라이버시 문제가 될 수 있습니다. 프로덕션에서는 민감한 정보를 마스킹하세요.

💡 candidate의 타입(host, srflx, relay)을 확인하여 연결 품질을 예측할 수 있습니다. host는 직접 연결(가장 빠름), srflx는 NAT 통과, relay는 TURN 서버 경유(가장 느림)를 의미합니다.

💡 모바일 환경에서는 네트워크 전환(Wi-Fi ↔ 모바일 데이터) 시 새로운 candidate가 생성됩니다. iceConnectionState를 모니터링하여 네트워크 변경에 대응하세요.


3. addIceCandidate 메서드

시작하며

여러분이 상대방으로부터 연결 정보를 받았을 때 이런 고민을 해본 적 있나요? "이 정보를 받긴 했는데, 이걸 어떻게 사용하지?" 하는 의문 말이죠.

이런 상황은 WebRTC 시그널링 과정에서 반드시 겪게 됩니다. 상대방이 자신의 네트워크 경로 정보(ICE Candidate)를 보내왔지만, 이를 우리 쪽 PeerConnection에 등록하지 않으면 연결이 성립되지 않습니다.

마치 친구가 자신의 주소를 문자로 보냈는데, 여러분이 그 주소를 내비게이션에 입력하지 않으면 찾아갈 수 없는 것과 같습니다. 바로 이럴 때 사용하는 것이 addIceCandidate 메서드입니다.

이 메서드는 상대방의 연결 경로 정보를 내 PeerConnection에 추가하여, 실제로 그 경로로 연결을 시도할 수 있게 해줍니다.

개요

간단히 말해서, addIceCandidate 메서드는 상대방으로부터 받은 ICE candidate를 로컬 PeerConnection에 등록하는 함수입니다. 시그널링 서버를 통해 상대방의 candidate를 받으면, 그 정보를 PeerConnection에 추가해야 합니다.

각 candidate는 상대방에게 연결할 수 있는 하나의 경로를 나타냅니다. 여러 candidate를 추가할수록 연결 성공 가능성이 높아지고, ICE 에이전트가 그중 최적의 경로를 선택합니다.

기존에는 모든 candidate를 모아서 한 번에 추가했다면, Trickle ICE 방식에서는 받는 즉시 하나씩 추가합니다. 이렇게 하면 첫 번째 candidate만으로도 연결이 가능한 경우 즉시 연결되어 전체 대기 시간이 줄어듭니다.

addIceCandidate의 핵심 특징은 첫째, 비동기 Promise 기반으로 동작한다는 점입니다. 둘째, remote description 설정 전에 호출하면 에러가 발생할 수 있습니다.

셋째, 잘못된 형식의 candidate를 추가하면 자동으로 무시됩니다. 이러한 특징을 이해하면 안정적인 연결 로직을 구현할 수 있습니다.

코드 예제

// 시그널링 서버로부터 candidate 수신
signalingServer.on('ice-candidate', async (data) => {
  try {
    // RTCIceCandidate 객체 생성
    const candidate = new RTCIceCandidate(data.candidate);

    // PeerConnection에 candidate 추가
    await peerConnection.addIceCandidate(candidate);

    console.log('상대방 ICE Candidate 추가 성공');
  } catch (error) {
    // Remote description 미설정 등의 에러 처리
    console.error('Candidate 추가 실패:', error);
  }
});

설명

이것이 하는 일: addIceCandidate 메서드는 상대방의 네트워크 경로 정보를 받아서 연결 가능한 옵션으로 추가하는 작업을 수행합니다. 첫 번째로, new RTCIceCandidate()를 사용하여 JSON 형식의 candidate 데이터를 RTCIceCandidate 객체로 변환합니다.

시그널링 서버를 통해 받은 데이터는 일반 JavaScript 객체이므로, 브라우저가 이해할 수 있는 RTCIceCandidate 타입으로 변환하는 과정이 필요합니다. 이 객체에는 candidate 문자열, sdpMid, sdpMLineIndex 같은 정보가 포함되어 있습니다.

그 다음으로, await peerConnection.addIceCandidate(candidate)를 호출하여 실제로 candidate를 추가합니다. 이 메서드는 Promise를 반환하므로 await를 사용하여 완료될 때까지 기다립니다.

내부적으로 ICE 에이전트는 이 새로운 경로에 대해 연결성 체크를 시작합니다. STUN 요청을 보내서 실제로 이 경로로 통신이 가능한지 테스트합니다.

try-catch 블록은 매우 중요합니다. 가장 흔한 에러는 setRemoteDescription()을 호출하기 전에 addIceCandidate()를 호출하는 경우입니다.

Remote description이 설정되지 않은 상태에서는 candidate를 어디에 매핑해야 할지 알 수 없어 에러가 발생합니다. 이런 경우를 대비해 candidate를 임시 큐에 저장했다가 remote description 설정 후 추가하는 방식을 사용할 수 있습니다.

여러분이 이 코드를 사용하면 상대방과의 연결 경로를 동적으로 확장할 수 있습니다. 네트워크 상황이 변하거나 더 좋은 경로가 발견되면 즉시 반영되어 연결 품질이 향상되고, 연결 실패 시 대체 경로로 자동 전환되어 안정성이 크게 개선됩니다.

실전 팁

💡 setRemoteDescription() 호출 전에 받은 candidate는 배열에 저장했다가 나중에 추가하세요. 많은 개발자가 순서 문제로 에러를 겪는데, 간단한 큐 패턴으로 해결할 수 있습니다.

💡 addIceCandidate()는 에러를 던져도 연결 자체는 계속 진행됩니다. 하나의 candidate 추가 실패가 전체 연결을 망치지 않도록 각 candidate를 독립적으로 처리하세요.

💡 null이나 undefined candidate는 추가하지 마세요. 일부 시그널링 구현에서 "수집 완료" 신호로 null을 보내는 경우가 있는데, 이를 그대로 addIceCandidate()에 전달하면 에러가 발생합니다.

💡 candidate의 sdpMid와 sdpMLineIndex를 확인하여 어느 미디어 스트림(오디오/비디오)에 대한 candidate인지 파악할 수 있습니다. 이 정보는 멀티 스트림 환경에서 디버깅에 유용합니다.

💡 프로덕션 환경에서는 candidate 추가 성공률을 메트릭으로 수집하세요. 성공률이 낮다면 시그널링 서버 문제, 네트워크 지연, 또는 방화벽 이슈를 의심할 수 있습니다.


4. Trickle ICE 구현

시작하며

여러분이 WebRTC 연결을 구현할 때 이런 불만을 느껴본 적 있나요? "연결되기까지 왜 이렇게 오래 걸리지?" 하는 답답함 말이죠.

이런 문제는 전통적인 ICE 구현 방식에서 자주 발생합니다. 모든 네트워크 경로를 다 찾을 때까지 기다렸다가 한 번에 교환하는 방식은 특히 네트워크 환경이 복잡할수록 시간이 오래 걸립니다.

10초 이상 걸리는 경우도 있어서 사용자들이 연결을 포기하는 일도 생깁니다. 바로 이럴 때 필요한 것이 Trickle ICE 방식입니다.

이 방식은 candidate를 찾는 즉시 하나씩 교환하여, 빠르면 1-2초 만에 연결을 완성할 수 있게 해줍니다.

개요

간단히 말해서, Trickle ICE는 ICE candidate를 발견하는 대로 즉시 상대방에게 전송하는 최적화 기법입니다. 전통적인 방식(Vanilla ICE)에서는 모든 candidate 수집이 완료될 때까지 기다린 후 SDP에 포함하여 한 번에 전송합니다.

하지만 Trickle ICE는 각 candidate를 독립적으로 즉시 전송합니다. 첫 번째 candidate만으로도 연결이 가능하면 바로 연결을 시작하고, 나머지 candidate는 백그라운드에서 계속 교환하여 더 좋은 경로가 있으면 전환합니다.

기존에는 offer/answer 교환 후 연결 시도했다면, Trickle ICE에서는 offer/answer와 candidate 교환이 동시에 진행됩니다. 이렇게 하면 전체 연결 시간이 절반 이하로 줄어듭니다.

Trickle ICE의 핵심 특징은 첫째, 점진적 연결(incremental connectivity)로 초기 연결 시간을 최소화합니다. 둘째, 병렬 처리로 네트워크 효율성을 높입니다.

셋째, 연결 후에도 계속 최적화가 가능합니다. 이러한 특징들이 현대 WebRTC 애플리케이션의 필수 요소가 된 이유입니다.

코드 예제

// Trickle ICE 구현
const pendingCandidates = [];
let remoteDescriptionSet = false;

// Offer 생성 및 전송
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingServer.send({ type: 'offer', sdp: offer });

// Candidate를 찾는 즉시 전송
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    signalingServer.send({ type: 'candidate', candidate: event.candidate });
  }
};

// 상대방 Answer와 Candidate를 받는 즉시 처리
signalingServer.on('answer', async (answer) => {
  await peerConnection.setRemoteDescription(answer);
  remoteDescriptionSet = true;

  // 대기 중이던 candidate 모두 추가
  pendingCandidates.forEach(c => peerConnection.addIceCandidate(c));
  pendingCandidates.length = 0;
});

signalingServer.on('candidate', async (data) => {
  const candidate = new RTCIceCandidate(data.candidate);

  if (remoteDescriptionSet) {
    await peerConnection.addIceCandidate(candidate);
  } else {
    // Remote description 대기 중이면 큐에 저장
    pendingCandidates.push(candidate);
  }
});

설명

이것이 하는 일: Trickle ICE 구현은 SDP 교환과 candidate 교환을 병렬로 처리하여 연결 시간을 최소화합니다. 첫 번째로, offer를 생성하고 즉시 전송합니다.

setLocalDescription()을 호출하면 내부적으로 ICE 수집이 시작되지만, 완료될 때까지 기다리지 않고 바로 offer를 전송합니다. 이때 offer의 SDP에는 아직 candidate가 포함되지 않았지만, 나중에 별도로 전송될 것이므로 문제없습니다.

이 방식으로 시그널링 지연을 줄일 수 있습니다. 그 다음으로, onicecandidate 이벤트 핸들러가 각 candidate를 발견하는 즉시 전송합니다.

이 과정은 offer/answer 교환과 동시에 진행됩니다. 예를 들어, host candidate는 즉시 발견되어 전송되고, srflx candidate는 STUN 서버 응답 후 전송되며, relay candidate는 TURN 서버 연결 후 전송됩니다.

각각 발견되는 즉시 상대방도 그 경로로 연결을 시도할 수 있습니다. pendingCandidates 배열과 remoteDescriptionSet 플래그는 타이밍 문제를 해결합니다.

상대방의 candidate가 answer보다 먼저 도착하는 경우가 있는데, remote description이 설정되지 않은 상태에서 addIceCandidate()를 호출하면 에러가 발생합니다. 따라서 일시적으로 큐에 저장했다가, answer 처리 후 모든 대기 중인 candidate를 한 번에 추가합니다.

이 패턴은 모든 Trickle ICE 구현에서 필수적입니다. 여러분이 이 코드를 사용하면 연결 시간이 극적으로 개선됩니다.

실제 측정 결과 평균 연결 시간이 8초에서 2초로 줄어든 사례도 있습니다. 또한 네트워크 품질이 낮은 환경에서도 일부 candidate만으로 연결을 시작하여 사용자 경험이 크게 향상됩니다.

실전 팁

💡 pendingCandidates 배열을 Set 대신 Array로 사용하는 이유는 순서가 중요하기 때문입니다. candidate의 우선순위 순서대로 추가해야 ICE가 효율적으로 동작합니다.

💡 시그널링 메시지 크기를 줄이려면 candidate를 배치(batch)로 모아서 전송할 수 있습니다. 예를 들어 100ms 동안 수집된 candidate를 배열로 묶어 전송하면 메시지 수를 줄이면서도 충분히 빠릅니다.

💡 iceGatheringState를 모니터링하여 UI에 진행 상황을 표시하세요. 'new' → 'gathering' → 'complete' 상태 변화를 사용자에게 보여주면 "연결 중" 상태에 대한 불안감이 줄어듭니다.

💡 모바일 환경에서는 배터리 절약을 위해 iceTransportPolicy를 동적으로 조정할 수 있습니다. 배터리가 부족하면 'relay' 옵션으로 전환하여 불필요한 candidate 수집을 건너뛸 수 있습니다.

💡 재연결 시나리오에서는 기존 candidate를 재사용할 수 있는지 확인하세요. iceRestart 옵션을 false로 설정하면 이전 ICE session의 candidate를 그대로 사용하여 연결 속도를 더욱 높일 수 있습니다.


5. ICE 연결 상태 확인

시작하며

여러분이 WebRTC 연결을 만들고 나서 이런 걱정을 해본 적 있나요? "지금 연결이 제대로 되어 있는 건가?

끊긴 건 아닐까?" 하는 불안감 말이죠. 이런 문제는 실시간 통신 애플리케이션에서 매우 중요합니다.

사용자가 화상 통화 중인데 네트워크가 끊겼는지도 모르고 계속 말하고 있다면, 이는 심각한 사용자 경험 문제입니다. 또한 개발자 입장에서도 연결 상태를 모니터링하지 않으면 문제가 발생했을 때 적절히 대응할 수 없습니다.

바로 이럴 때 사용하는 것이 ICE 연결 상태 모니터링입니다. iceConnectionState와 connectionState 이벤트를 통해 실시간으로 연결 상태를 추적하고, 문제 발생 시 즉각 대응할 수 있습니다.

개요

간단히 말해서, ICE 연결 상태 확인은 PeerConnection의 현재 연결 상태를 실시간으로 모니터링하고 변화에 대응하는 기법입니다. WebRTC는 연결의 생명주기 동안 여러 상태를 거칩니다.

'new'에서 시작하여 'checking'(연결 시도), 'connected'(연결 성공), 'completed'(최적화 완료), 'failed'(실패), 'disconnected'(일시적 끊김), 'closed'(종료) 등의 상태로 전환됩니다. 각 상태는 특정 의미를 가지며, 적절한 UI 업데이트나 재연결 로직을 트리거해야 합니다.

기존에는 단순히 연결 성공/실패만 확인했다면, 현대적인 구현에서는 모든 상태 변화를 추적하여 세밀한 제어를 합니다. 예를 들어 'disconnected'는 일시적 문제이므로 자동 재연결을 시도하지만, 'failed'는 완전한 실패이므로 전체 연결을 재시작해야 합니다.

ICE 상태 모니터링의 핵심 특징은 첫째, 네트워크 변화를 실시간으로 감지할 수 있다는 점입니다. 둘째, 사용자에게 명확한 피드백을 제공할 수 있습니다.

셋째, 자동 복구 로직을 구현할 수 있습니다. 이러한 특징들이 안정적인 프로덕션 애플리케이션의 기반이 됩니다.

코드 예제

// ICE 연결 상태 모니터링
peerConnection.oniceconnectionstatechange = () => {
  const state = peerConnection.iceConnectionState;
  console.log('ICE Connection State:', state);

  switch (state) {
    case 'checking':
      updateUI('연결 확인 중...');
      break;
    case 'connected':
    case 'completed':
      updateUI('연결됨');
      startConnectionTimer(); // 연결 시간 측정
      break;
    case 'disconnected':
      updateUI('연결 끊김 - 재연결 시도 중');
      attemptReconnection(); // 자동 재연결
      break;
    case 'failed':
      updateUI('연결 실패');
      handleConnectionFailure(); // 전체 재시작
      break;
    case 'closed':
      updateUI('연결 종료');
      cleanup(); // 리소스 정리
      break;
  }
};

// 추가: 전반적인 연결 상태 (더 신뢰성 있음)
peerConnection.onconnectionstatechange = () => {
  console.log('Connection State:', peerConnection.connectionState);
};

설명

이것이 하는 일: ICE 연결 상태 모니터링은 네트워크 연결의 변화를 감지하고 적절히 대응하여 안정적인 서비스를 제공합니다. 첫 번째로, oniceconnectionstatechange 이벤트는 ICE 에이전트의 연결 상태가 변경될 때마다 발생합니다.

이 이벤트는 매우 민감하여 네트워크 패킷 손실, Wi-Fi 신호 약화, 모바일 데이터 전환 등 다양한 상황에서 트리거됩니다. iceConnectionState 속성을 확인하여 현재 어떤 상태인지 파악합니다.

그 다음으로, switch 문으로 각 상태별 처리를 분기합니다. 'checking' 상태는 candidate 쌍을 테스트하는 중이므로 로딩 인디케이터를 표시합니다.

'connected'는 최소 하나의 경로가 성공했다는 의미이고, 'completed'는 모든 candidate 확인이 끝나고 최적 경로를 선택했다는 의미입니다. 둘 다 정상 연결이므로 같이 처리합니다.

'disconnected' 상태는 특별한 주의가 필요합니다. 이는 일시적 네트워크 문제로 연결이 끊겼지만, 아직 완전히 실패한 것은 아닙니다.

ICE 에이전트가 자동으로 재연결을 시도하지만, 여러분이 추가로 attemptReconnection() 같은 로직을 구현할 수 있습니다. 예를 들어 createOffer({ iceRestart: true })를 호출하여 ICE를 재시작하는 방법이 있습니다.

'failed' 상태는 모든 candidate 쌍이 실패하여 더 이상 연결할 방법이 없다는 의미입니다. 이 경우 전체 PeerConnection을 닫고 새로 시작하는 것이 일반적입니다.

connectionState도 함께 모니터링하면 더 정확한 판단이 가능합니다. 여러분이 이 코드를 사용하면 사용자에게 명확한 상태 피드백을 제공할 수 있습니다.

"연결 중", "연결됨", "재연결 중" 같은 메시지로 사용자 불안감을 해소하고, 자동 복구 로직으로 대부분의 일시적 문제를 사용자 개입 없이 해결할 수 있습니다. 또한 연결 품질 메트릭을 수집하여 서비스 개선에 활용할 수 있습니다.

실전 팁

💡 'disconnected'에서 'connected'로 자동 복구되는 경우가 많으므로, 즉시 에러 메시지를 표시하지 말고 3-5초 정도 유예 시간을 두세요. 너무 빠른 에러 표시는 사용자를 혼란스럽게 합니다.

💡 connectionState와 iceConnectionState를 함께 확인하세요. connectionState는 DTLS와 ICE를 모두 고려하므로 더 정확합니다. 두 상태가 불일치하면 로그를 남겨 디버깅에 활용하세요.

💡 iceConnectionState가 'failed'가 되면 getStats() API로 어떤 candidate가 실패했는지 확인할 수 있습니다. 이 정보로 방화벽, TURN 서버 문제, NAT 타입 등을 진단할 수 있습니다.

💡 재연결 로직에는 exponential backoff를 적용하세요. 즉시 재시도하면 서버 부하가 커지므로, 첫 시도 1초, 두 번째 2초, 세 번째 4초 식으로 간격을 늘리는 것이 좋습니다.

💡 모바일 앱에서는 'disconnected' 상태가 앱이 백그라운드로 갔을 때 발생할 수 있습니다. visibilitychange 이벤트와 함께 처리하여 불필요한 재연결을 방지하세요.


6. NAT 트래버설

시작하며

여러분이 WebRTC 연결을 시도할 때 이런 좌절을 경험한 적 있나요? "로컬에서는 잘 되는데 외부 네트워크에서는 왜 안 되지?" 하는 당혹감 말이죠.

이런 문제는 대부분 NAT(Network Address Translation) 때문에 발생합니다. 회사나 집의 공유기는 내부 네트워크를 보호하기 위해 외부에서 직접 접근할 수 없게 만듭니다.

이는 보안상 좋지만, P2P 연결을 시도하는 WebRTC에는 큰 장벽이 됩니다. 마치 아파트 경비실을 거치지 않고 직접 방문하려는 것과 같습니다.

바로 이럴 때 필요한 것이 NAT 트래버설(NAT Traversal) 기술입니다. STUN과 TURN 서버를 활용하여 NAT 뒤의 기기들도 서로 연결할 수 있게 해주는 기법입니다.

개요

간단히 말해서, NAT 트래버설은 방화벽이나 NAT 뒤에 있는 기기들이 서로 직접 통신할 수 있도록 하는 기술입니다. NAT는 하나의 공인 IP 주소를 여러 기기가 공유하게 만듭니다.

예를 들어 집 Wi-Fi에 연결된 모든 기기는 외부에서 볼 때 같은 IP 주소를 가집니다. 이 때문에 외부 기기는 내부의 특정 기기에 직접 연결할 수 없습니다.

STUN 서버는 여러분의 공인 IP와 포트를 알려주고, TURN 서버는 직접 연결이 불가능할 때 중계 역할을 합니다. 기존에는 중앙 서버를 통한 중계만 사용했다면, 현대 WebRTC는 ICE 프레임워크로 가능한 경우 직접 연결하고 불가능한 경우만 중계합니다.

이렇게 하면 비용과 지연 시간을 크게 줄일 수 있습니다. NAT 트래버설의 핵심 특징은 첫째, 여러 NAT 타입(Cone, Symmetric 등)에 대응할 수 있다는 점입니다.

둘째, STUN으로 80% 이상의 경우 직접 연결이 가능하고, 나머지는 TURN으로 보장합니다. 셋째, ICE가 자동으로 최선의 방법을 선택합니다.

이러한 특징들이 WebRTC의 높은 연결 성공률을 만듭니다.

코드 예제

// NAT 트래버설을 위한 STUN/TURN 서버 설정
const configuration = {
  iceServers: [
    // STUN: 공인 IP 확인 (무료, 대역폭 적음)
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },

    // TURN: 직접 연결 실패 시 중계 (유료, 대역폭 많음)
    {
      urls: [
        'turn:turn.example.com:3478?transport=udp',
        'turn:turn.example.com:3478?transport=tcp'
      ],
      username: 'username',
      credential: 'password'
    }
  ],
  // NAT 통과 가능성을 높이는 옵션
  iceCandidatePoolSize: 10,
  bundlePolicy: 'max-bundle', // 모든 미디어를 하나의 연결로
  rtcpMuxPolicy: 'require' // RTP와 RTCP를 같은 포트로
};

const pc = new RTCPeerConnection(configuration);

// Candidate 타입별 연결 성공 확인
pc.onicecandidate = (event) => {
  if (event.candidate) {
    const type = event.candidate.type; // host, srflx, relay
    console.log(`Candidate 타입: ${type}`);

    // srflx: STUN 성공 (NAT 통과), relay: TURN 사용 (중계)
  }
};

설명

이것이 하는 일: NAT 트래버설 설정은 다양한 네트워크 환경에서도 연결이 성공할 수 있도록 여러 전략을 준비합니다. 첫 번째로, STUN 서버 설정은 여러분의 공인 IP 주소와 포트 번호를 알아내는 역할을 합니다.

브라우저는 STUN 서버에 요청을 보내고, STUN 서버는 "당신의 공인 주소는 203.0.113.5:54321입니다"라고 답변합니다. 이 정보로 만들어진 candidate가 srflx(server reflexive) 타입입니다.

STUN 서버는 단순히 주소만 알려주므로 거의 대역폭을 사용하지 않아 무료 서버를 사용해도 됩니다. 여러 STUN 서버를 설정하면 한 서버가 다운되어도 다른 서버를 사용합니다.

그 다음으로, TURN 서버는 직접 연결이 완전히 불가능한 경우(Symmetric NAT 등)에 사용됩니다. TURN 서버는 모든 미디어 데이터를 중계하므로 엄청난 대역폭을 사용합니다.

따라서 인증(username, credential)이 필수이며, 보통 유료 서비스를 사용합니다. transport=udp와 transport=tcp를 모두 설정하면, UDP가 차단된 기업 방화벽에서도 TCP로 연결할 수 있습니다.

iceCandidatePoolSize: 10은 연결 전에 미리 candidate를 수집합니다. bundlePolicy: 'max-bundle'은 오디오와 비디오를 하나의 네트워크 연결로 묶어 NAT 포트 사용을 줄입니다.

rtcpMuxPolicy: 'require'는 RTP(미디어 데이터)와 RTCP(제어 데이터)를 같은 포트로 보내 방화벽 통과 확률을 높입니다. onicecandidate 핸들러에서 candidate.type을 확인하면 어떤 방법으로 연결되는지 알 수 있습니다.

'host'는 같은 네트워크 내 직접 연결, 'srflx'는 STUN을 통한 NAT 통과, 'relay'는 TURN 중계를 의미합니다. 통계를 수집하면 여러분의 사용자 중 몇 퍼센트가 TURN을 사용하는지 알 수 있어 서버 용량을 계획하는 데 도움이 됩니다.

여러분이 이 코드를 사용하면 거의 모든 네트워크 환경에서 연결에 성공할 수 있습니다. 실제 통계에 따르면 STUN만으로 약 86%의 연결이 성공하고, TURN을 추가하면 99% 이상 성공합니다.

이는 프로덕션 WebRTC 서비스의 필수 요소입니다.

실전 팁

💡 TURN 서버 비용을 줄이려면 먼저 STUN으로 연결을 시도하고, 실패한 경우만 TURN을 사용하세요. iceTransportPolicy를 'all'(기본값)로 두면 자동으로 이렇게 동작합니다.

💡 자체 TURN 서버를 운영하려면 coturn 오픈소스를 추천합니다. AWS EC2 t3.medium 인스턴스 하나로 수백 명의 동시 연결을 처리할 수 있습니다. Docker로 쉽게 배포 가능합니다.

💡 TURN 서버는 지역별로 배치하세요. 한국 사용자는 한국 TURN 서버를, 미국 사용자는 미국 TURN 서버를 사용하면 지연 시간이 크게 줄어듭니다. geolocation API나 IP 기반으로 가장 가까운 서버를 선택하세요.

💡 Symmetric NAT 환경(일부 기업 방화벽)에서는 TURN이 필수입니다. 엔터프라이즈 고객을 대상으로 한다면 반드시 TURN 서버를 준비하고, 연결 성공률을 모니터링하세요.

💡 TURN 서버 credential은 시간 제한(TTL)을 설정하여 보안을 강화하세요. 예를 들어 24시간 유효한 임시 credential을 생성하면, credential이 유출되어도 피해를 최소화할 수 있습니다. HMAC 기반 인증을 사용하세요.


#WebRTC#ICE#PeerConnection#NAT#Trickle#WebRTC,ICE

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

WebRTC 화면 공유 완벽 가이드

WebRTC의 화면 공유 기능을 처음부터 끝까지 배워봅니다. getDisplayMedia API 사용법부터 화면과 카메라 동시 표시, 공유 중단 감지, 해상도 설정까지 실무에서 바로 활용할 수 있는 모든 것을 다룹니다.

WebRTC Mesh 방식 다자간 화상회의 완벽 가이드

다자간 화상회의를 구현하는 Mesh, SFU, MCU 방식의 차이부터 실제 구현까지. PeerConnection 관리, 참여자 추가/제거, 대역폭 최적화까지 실무에 바로 적용할 수 있는 완벽한 가이드입니다.

1:1 화상 통화 구현 완벽 가이드

WebRTC를 활용한 1:1 화상 통화 시스템을 처음부터 끝까지 구현하는 방법을 배워봅니다. 통화 시작부터 종료까지, 음소거와 카메라 제어, 연결 품질 모니터링까지 실무에 바로 적용할 수 있는 모든 과정을 다룹니다.

WebRTC RTCPeerConnection 생성과 관리 완벽 가이드

WebRTC의 핵심인 RTCPeerConnection을 생성하고 관리하는 방법을 배웁니다. ICE 서버 설정부터 미디어 트랙 추가, 이벤트 관리, 연결 상태 모니터링까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.

Socket.io로 배우는 WebRTC 시그널링 서버 구축

WebRTC 연결을 위한 필수 요소인 시그널링 서버를 Socket.io로 구축하는 방법을 배웁니다. Room 관리, Offer/Answer 교환, ICE Candidate 전달 등 실시간 통신의 핵심 개념을 실무 예제와 함께 친절하게 설명합니다.