[WebRTC 딥다이브] WebRTC 연결은 왜 그냥 열리지 않을까
WebRTC 연결은 버튼 하나로 열리는 것처럼 보인다. 브라우저 API를 호출하고 상대에게 offer와 answer를 주고받으면 끝날 것 같다. 실제로는 그 사이에 꽤 많은 조건이 숨어 있다. 두 사용자가 모두 인터넷에 접속해 있다는 사실과, 서로에게 패킷을 보낼 수 있다는 사실은 다르다. NAT, 방화벽, 회사망, 모바일망, 브라우저 권한, UDP 차단이 모두 연결을 흔든다. 같은 입력인데 어떤 환경에서만 WebRTC가 도드라졌다. 숫자로 포장하기보다, 재현 조건과 운영에서 고정한 규칙을 중심으로 적었다.

연결이 안 열려서 다시 본 이유
사용자 문구는 내부 로그보다 더 신중해야 한다. ICE failed를 그대로 보여 주면 대부분의 사용자는 아무 행동도 못 한다. “직접 연결을 시도하는 중”, “우회 경로로 전환하는 중”, “브라우저 권한을 확인해 주세요”, “회사망에서 직접 연결이 막혀 중계 경로를 사용합니다”처럼 다음 행동이나 상태를 알려 주는 문장이 낫다. 물론 너무 많은 기술 설명을 화면에 올리면 피곤하다.
기본 문구는 짧게, 상세 원인은 지원이나 로그에서 볼 수 있게 나누는 편이 좋다. 그래서 WebRTC 기능을 출시할 때는 성공 경로보다 실패 이름을 먼저 정해 두는 편이 낫다. permission_denied, signaling_timeout, ice_failed, turn_auth_failed, datachannel_backpressure, receiver_write_slow처럼 단계가 나뉘어 있으면 개선 순서가 보인다. 사용자는 내부 이름을 몰라도 된다. 하지만 제품은 그 이름을 알고 있어야 적절한 안내와 재시도를 할 수 있다. 개발자 도구에서는 브라우저의 webrtc-internals 같은 자료가 큰 도움이 된다.
다만 사용자에게 그 화면을 보내 달라고 하는 것은 마지막 수단이어야 한다. 제품 자체가 기본적인 연결 단계와 candidate type, relay 여부, 전송량을 기록해야 한다. 그래야 문의 하나를 해결하려고 사용자를 디버깅 과정에 끌어들이지 않아도 된다. 좋은 관측은 사용자에게 덜 귀찮은 지원 경험을 만든다. 재연결 정책에서는 사용자 행동도 고려해야 한다.
사용자가 파일을 보내는 중이라면 연결이 끊겼을 때 같은 파일을 이어 보낼 수 있어야 한다. 통화 중이라면 잠깐 끊긴 뒤 자동으로 복구되는 것이 좋다. 화면 공유 중이라면 복구 뒤 같은 track을 다시 공유해야 할 수도 있다. WebRTC API만으로 이 경험이 완성되지는 않는다. 애플리케이션 상태를 어떻게 저장하고 복원할지 정해야 한다.

처음에는 WebRTC면 자동으로 열릴 줄 알았다
현장에서 자주 생기는 문제는 성공 로그가 너무 낙관적이라는 점이다. connected가 찍혔다고 사용자가 곧바로 만족하는 것은 아니다. data channel은 열렸지만 첫 chunk가 늦을 수 있고, 미디어 track은 붙었지만 bitrate가 거의 없을 수 있다. ICE state와 실제 전송 상태를 같이 봐야 한다. 연결 성공과 사용 가능한 연결은 다르다. 나는 WebRTC 연결을 테스트할 때 같은 Wi-Fi 성공만으로 안심하면 안 된다고 배웠다.
같은 사무실, 모바일 테더링, 회사 VPN, 공용 와이파이, 해외 region, UDP가 막힌 환경을 나눠 봐야 한다. 좋은 네트워크에서는 거의 모든 설계가 그럴듯하다. 문제는 사용자가 제품을 좋은 네트워크에서만 쓰지 않는다는 점이다. 오래 켜 둔 탭, 절전 모드에서 깨어난 노트북, 이동 중인 모바일도 실제 사용 환경이다. 작게 구현할 때도 이 구조를 염두에 두면 코드가 덜 꼬인다.
signaling 상태, ICE 상태, channel 상태, 전송 상태를 한 boolean으로 합치지 않는 것이다. 화면에는 단순한 문구 하나를 보여 주더라도 내부 상태는 분리해 둬야 한다. 그래야 나중에 TURN 추가, 재연결, 이어받기, 네트워크별 안내를 붙일 때 기존 코드를 계속 뜯어고치지 않는다. 처음 WebRTC를 공부할 때 가장 위험한 오해는 “P2P니까 서버가 거의 필요 없다”는 생각이었다. 미디어나 데이터가 직접 흐를 수는 있지만, 두 브라우저가 서로를 찾고 연결 후보를 시험하려면 signaling이 필요하다.
외부에서 보이는 주소를 알아내려면 STUN이 필요하고, 직접 경로가 막히면 TURN relay가 필요하다. 서버가 payload를 항상 들고 있지 않더라도, 연결을 성립시키는 과정에는 여러 보조 장치가 들어간다. TURN credential 관리도 작은 주제가 아니다. relay 서버는 비용이 드는 자원이라 아무나 오래 쓰게 두면 안 된다. 짧은 수명의 credential을 발급하고, 방 권한과 맞추고, 만료된 세션이 계속 relay를 쓰지 못하게 해야 한다.
보안이 약하면 비용 공격이나 무단 사용으로 이어질 수 있다. 연결 안정성만 보다가 relay 접근 제어를 놓치면 나중에 운영비로 돌아온다.

오해가 만든 잘못된 구현
NAT가 문제를 어렵게 만든다. 집 공유기 뒤의 기기는 내부 주소를 갖고 있고, 외부에서는 그 주소로 바로 들어올 수 없다. STUN은 바깥에서 보이는 주소를 알려 주는 역할을 한다. 하지만 모든 NAT가 친절하지는 않다. symmetric NAT나 엄격한 방화벽에서는 직접 경로가 잘 안 열린다. 이때 TURN은 중계 서버로 동작한다. 성공률은 올라가지만 비용과 지연이 생긴다. 그래서 TURN은 보험이면서 동시에 비용 항목이다.
로그는 단계별로 남겨야 한다. signaling 메시지가 오갔는지, ICE gathering이 끝났는지, 어떤 candidate type이 선택됐는지, connection state가 어디서 멈췄는지, data channel이 열린 뒤 bufferedAmount가 쌓이는지 확인해야 한다. 미디어라면 bitrate와 packet loss를 보고, 파일 전송이라면 chunk 크기와 backpressure를 같이 본다. 연결 성공률 하나만 보면 늦다. 어느 단계에서 오래 걸렸는지가 더 중요하다. 흐름을 나눠 보면 이해가 덜 막힌다. 먼저 signaling으로 offer와 answer를 교환한다.
이 단계는 WebRTC 표준이 직접 정해 주지 않는다. 웹소켓이든 HTTP든 제품이 직접 만든 통로를 쓴다. 그 다음 ICE candidate를 모은다. 브라우저는 host candidate, server-reflexive candidate, relay candidate 같은 여러 후보를 만들 수 있다. 마지막으로 후보 쌍을 시험해서 실제로 패킷이 오가는 경로를 고른다. 사용자는 이 모든 과정을 “연결 중” 한 줄로 본다. 모바일에서는 네트워크 변경이 특히 흔하다. Wi-Fi에서 LTE로 바뀌거나, 지하철에서 신호가 약해지거나, 절전 모드가 켜진다.
이때 연결이 완전히 끊어질 수도 있고, 잠깐 멈췄다가 돌아올 수도 있다. 큰 파일 전송이나 긴 상담 세션에서는 이 차이가 크다. 짧은 데모에서는 보이지 않는 문제가 실제 사용 시간에서는 반복된다. 짧게 정리하면 WebRTC는 자동으로 NAT를 “뚫어 주는” 기술이 아니다. 브라우저가 후보를 모으고 시험하며, 필요하면 TURN으로 우회한다. 어떤 네트워크에서는 잘 되고 어떤 곳에서는 안 되는 이유도 여기에 있다.
NAT 유형, 방화벽 정책, UDP 허용 여부, TURN 설정, 브라우저 권한이 모두 다르다. WebRTC 연결을 더 자세히 보면 실패가 한 번에 생기는 것이 아니라 단계마다 조금씩 갈라진다. signaling 서버에 접속하지 못하면 상대에게 offer 자체가 가지 않는다. offer와 answer는 오갔지만 candidate가 늦게 오거나 누락되면 후보 검사가 제대로 시작되지 않는다. candidate는 충분한데 네트워크가 UDP를 막으면 직접 경로가 실패한다.
TURN이 없거나 인증이 틀리면 우회도 실패한다. 사용자는 모두 “연결이 안 된다”고 말하지만, 운영자는 이 네 가지를 따로 봐야 한다. WebRTC를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
연결 과정의 경계 다시 그리기
파일 전송에서는 연결 이후의 흐름 제어가 중요해진다. WebRTC DataChannel이 열렸다고 곧바로 큰 파일을 밀어 넣으면 브라우저 버퍼가 쌓인다. bufferedAmount를 보면서 chunk 크기와 전송 속도를 조절해야 한다. 수신자가 디스크에 쓰는 속도도 영향을 준다. 그래서 연결 문제와 전송 문제를 분리해야 한다. 연결은 성공했지만 backpressure를 무시해서 멈춘 경우를 ICE 실패로 기록하면 다음 개선 방향이 틀어진다. 연결 실패라는 한 문장 안에는 권한 거부, 후보 교환 누락, UDP 차단, TURN 미설정이 같이 숨어 있다.
브라우저 권한도 별개의 실패다. 카메라와 마이크 권한을 거부한 것과 ICE 연결이 실패한 것은 사용자 화면에서는 모두 “통화가 안 됨”처럼 보일 수 있다. 데이터 채널 파일 전송이라면 미디어 권한은 필요 없지만, 연결 후보와 네트워크 제약은 여전히 문제다. 제품이 이 차이를 구분하지 않으면 사용자는 권한을 다시 눌러야 하는지, 네트워크를 바꿔야 하는지, 상대를 다시 초대해야 하는지 알 수 없다. ICE candidate 이름도 처음에는 낯설다. host candidate는 기기 내부 주소에 가깝고, server-reflexive candidate는 STUN을 통해 바깥에서 보이는 주소를 알아낸 것이다. relay candidate는 TURN 서버를 거치는 주소다. 브라우저는 이 후보들을 조합해 실제로 패킷이 오가는지 시험한다. 그래서 candidate가 많다고 항상 좋은 것도 아니다. 후보가 많으면 검사 시간이 길어질 수 있고, 잘못된 우선순위가 있으면 느린 경로를 오래 붙잡을 수 있다.
연결이 맺어지는 순서
연결 재시도 정책도 빼놓기 어렵다. ICE가 잠깐 실패했다고 바로 세션을 닫으면 사용자는 파일이나 통화를 처음부터 다시 시작해야 한다. 반대로 끝없이 재시도하면 실패가 길게 늘어진다. 몇 초 동안 직접 경로를 시도하고, 언제 relay로 넘어가며, 언제 사용자에게 네트워크 변경을 안내할지 기준을 둬야 한다. 결국 WebRTC 연결이 어렵다는 말은 API가 복잡하다는 뜻만이 아니다.
브라우저, 네트워크, 서버, 권한, 비용, 사용자 문구가 한꺼번에 맞아야 한다는 뜻이다. 연결이 열리는 순간보다 연결이 흔들릴 때의 처리가 제품 품질을 가른다. 좋은 환경에서 한 번 성공하는 데서 멈추지 않고, 나쁜 환경에서 어떤 이름으로 실패하는지 기록해야 WebRTC가 서비스 안에서 버틴다. *연결 문제는 권한, signaling, ICE, TURN, media/data channel 상태로 나눠야 원인을 찾을 수 있다.
마지막으로 QA 환경을 일부러 불편하게 만들어야 한다. 같은 책상 위 노트북 두 대는 가장 쉬운 시나리오다. 모바일 핫스팟, VPN, 방화벽이 있는 회사망, 해외 region, 잠자기에서 깨어난 기기, 오래 열린 탭을 넣어야 실제 제품에 가까워진다. WebRTC는 좋은 환경에서 놀랄 만큼 잘 되지만, 나쁜 환경에서는 실패 이름을 잘 붙인 제품만이 사용자에게 다음 행동을 알려 줄 수 있다.에서는 미디어 서버 선택을 봤다.에서는 연결 과정에 자주 등장하는 세 단어를 따로 떼어 정리한다.
WebRTC 연결을 이해한다는 것은 API 이름을 외우는 것보다, 실패가 어느 단계에서 생기는지 작게 나눠 보는 일에 더 가깝다. '[WebRTC 딥다이브 WebRTC 연결은 왜 그냥 열리지 않을까'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
브라우저 상태 변화도 연결을 흔든다. 사용자가 탭을 백그라운드로 보내거나 노트북을 잠깐 덮거나 모바일에서 앱을 전환하면 타이머와 네트워크 상태가 달라질 수 있다. 연결이 열렸다고 영원히 안정적인 것은 아니다. ICE restart, reconnect, data channel 재개, 파일 전송 이어받기 같은 정책이 필요하다. 특히 큰 파일을 보내는 기능에서는 연결이 잠깐 흔들렸다고 처음부터 다시 보내게 하면 사용자가 금방 지친다.
보안 측면에서도 signaling과 연결 상태를 따로 봐야 한다. 방에 들어올 권한이 있는 사용자만 candidate를 교환해야 하고, 만료된 세션이 오래 살아 있지 않도록 정리해야 한다. WebRTC 연결은 브라우저 사이의 기술처럼 보이지만, 실제 제품에서는 권한 서버, signaling 서버, TURN 서버, 클라이언트 상태 관리가 한꺼번에 맞아야 한다. realtime-network-15- 권한 실패와 네트워크 실패는 화면 문구부터 달라야 한다.
카메라 권한을 거부했다면 사용자가 브라우저 설정을 바꿔야 한다. 회사망에서 UDP가 막혔다면 다른 네트워크나 relay 전환이 필요하다. 상대가 방을 닫았거나 signaling 세션이 만료됐다면 다시 초대해야 한다. 하나의 “연결 실패” 문구는 개발자에게 편하지만 사용자에게는 불친절하다. TURN은 마지막 보험처럼 생각하기 쉽지만 운영에서는 핵심 인프라가 될 때가 많다.
회사망이나 학교망, 호텔 와이파이처럼 직접 연결이 어려운 환경에서는 TURN이 없으면 기능이 불안정해진다. 대신 TURN을 많이 쓰면 서버 egress 비용이 커진다. 그러니 TURN 사용률은 단순 실패 대응 지표가 아니라 비용 지표이기도 하다. 어느 네트워크에서 relay 비율이 높은지 알아야 가격 정책과 인프라 계획을 세울 수 있다. 관측할 때는 시간값을 꼭 남겨야 한다.
방 입장 시각, offer 생성 시각, answer 수신 시각, 첫 candidate 도착 시각, ICE connected 시각, data channel open 시각을 분리하면 어느 구간이 느린지 보인다. p95만 보는 것도 부족하다. p50은 괜찮은데 p99가 길다면 특정 네트워크나 region에서만 문제가 생기는 것일 수 있다. 사용자가 느끼는 불편은 평균보다 꼬리 구간에서 더 자주 나온다. WebRTC 점검은 API 호출 여부보다 실패를 분리해 관측할 수 있는지에 달려 있다.
제품 기준은 네 가지로 잡을 수 있다. 권한 오류와 네트워크 오류를 분리한다. ICE state와 candidate type을 로그에 남긴다. TURN fallback을 실제 환경에서 테스트한다. 회사망, 모바일망, 가정망을 나눠 QA한다. 이 기준 없이 WebRTC 기능을 내면 로컬에서는 잘 되는데 고객 환경에서만 실패하는 문제를 계속 만난다. PonsWarp 같은 파일 전송 기능에서는 이 차이가 더 중요하다. 사용자는 파일이 안 가면 “전송 실패”로만 느끼지만 내부적으로는 signaling 실패, 직접 경로 실패, relay 전환, 수신자 저장 속도 부족, backpressure 문제가 다를 수 있다. PonsLink의 실시간 세션도 마찬가지다. 어떤 단계에서 멈췄는지 알아야 제품 버그와 네트워크 제약을 구분할 수 있다. WebRTC를 쓴다는 것은 연결 실패를 더 자세히 설명할 책임도 같이 진다는 뜻이다. WebRTC 연결은 약속 교환, 후보 수집, 후보 검사, 실제 전송이 순서대로 맞아야 열린다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.