[WebRTC 딥다이브] P2P에서 signaling 서버는 왜 필요한가
WebRTC 예제를 처음 따라 칠 때 막힌 곳은 카메라나 파일 전송이 아니었다. 두 브라우저가 서로 어떤 약속을 주고받아야 하는지, 그 약속을 어디로 보내야 하는지가 먼저 막혔다. WebRTC가 연결과 미디어 처리의 큰 틀을 제공해도 signaling 서버를 대신 만들어 주지는 않는다. 어떤 정보를 어떤 프로토콜로 누구에게 전달할지는 애플리케이션이 정해야 한다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, WebRTC 주변에서 반복되던 작은 끊김이었다. 그래서 용어 사전 대신, 그 날 무엇을 보고 무엇을 바꿨는지부터 적는다.

signaling 없이 연결이 안 열린 이유
보안 로그와 개인정보 로그를 나누는 것도 좋다. 누가 어느 방에 들어왔고 어떤 권한으로 SDP 전달이 허용됐는지는 보안 로그에 가깝다. candidate type이나 연결 시간은 품질 로그에 가깝다. 둘을 한 덩어리로 오래 보관하면 관리가 어려워진다. 필요한 보존 기간과 접근 권한을 다르게 잡으면 나중에 감사와 디버깅이 충돌할 가능성이 줄어든다. 관측값도 메시지 단위로 남겨야 한다.
방 ID, peer ID, signaling session ID, offer/answer 생성 시각, candidate 개수, 버퍼링된 candidate 수, 권한 확인 결과, 재접속 횟수, 닫힌 이유를 기록한다. payload가 몇 GB든 signaling 메시지는 몇 KB에서 몇십 KB일 수 있다. 그래도 이 작은 메시지가 없으면 큰 데이터 경로는 시작되지 않는다. 작다고 대충 다루면 장애 원인을 찾기 어렵다. 로그 설계에서는 payload 내용을 남기지 않아도 충분히 많은 것을 볼 수 있다. 어떤 peer가 offer를 만들었는지, answer가 몇 초 뒤 도착했는지, candidate가 몇 개 버퍼링됐는지, 연결이 어떤 이유로 닫혔는지 남기면 된다.
이 정보만으로도 대부분의 signaling 문제를 좁힐 수 있다. 반대로 SDP 전체를 무분별하게 저장하면 개인정보와 보안 부담이 커진다. 필요한 값만 구조화해서 남기는 편이 낫다. signaling 점검은 offer/answer 순서, candidate 처리, 권한 확인, 재접속 정리에서 시작한다. 특히 WebRTC에서는 재현 조건을 같이 적어야 의미가 있었다.

섞기 쉬운 인접 개념
문서에는 “서버가 payload를 보지 않는다”와 “서버가 signaling을 책임진다”를 함께 적어야 한다. 앞문장만 있으면 사용자는 서버가 아무 역할도 하지 않는다고 오해할 수 있고, 뒷문장만 있으면 직접 전송의 장점이 흐려진다. 둘을 같이 말하면 경계가 분명하다. 서버는 방과 약속의 질서를 지키고, 브라우저는 선택된 경로로 데이터를 옮긴다. 이 경계가 WebRTC 제품을 설명하는 가장 중요한 문장 중 하나다.
*signaling 실패는 연결 경로 문제가 아니라 약속의 순서와 소유자가 흐려질 때 자주 생긴다. 클라이언트 코드에서도 signaling 이벤트를 아무 곳에서나 처리하지 않는 편이 좋다. PeerConnection 상태와 방 상태를 함께 보는 작은 레이어를 두고, 그 레이어가 처리 가능한 메시지만 통과시키게 한다. UI 컴포넌트가 직접 candidate를 넣거나 offer를 만들기 시작하면 나중에 재접속과 권한 변경을 다루기 어렵다.
signaling은 네트워크 코드처럼 보이지만 실제로는 세션 도메인 코드에 가깝다. 사용자 지원 문서에도 signaling 흔적이 남는다. “상대가 방에 들어왔지만 연결이 시작되지 않아요”는 미디어 문제가 아니라 signaling 문제일 수 있다. “연결 중에서 멈춰요”는 candidate 교환이나 ICE 상태 문제일 수 있다. 지원팀이 이런 상태 이름을 알면 문의를 더 빨리 분류한다. 개발자 내부 용어를 그대로 노출하지 않더라도 사건의 단계는 공유해야 한다.
재접속 처리는 사용자에게 가장 자주 보이는 부분이다. 네트워크가 잠깐 끊겼다가 돌아왔을 때 방에서 내보낼지, 같은 peer로 복귀시킬지, 새 peer로 등록할지 정해야 한다. 이전 candidate와 새 candidate가 섞이면 연결이 이상한 상태로 남을 수 있다. 서버가 세션 세대를 구분해 주면 오래된 메시지를 버리기 쉽다. 이 작은 구분 하나가 재현하기 어려운 버그를 많이 줄인다.

signaling이 지켜야 할 것
처음에는 signaling 서버를 단순 메시지 중계기로 생각했다. A가 보낸 offer를 B에게 전달하고, B의 answer를 다시 A에게 전달하면 끝이라고 여겼다. 작은 데모에서는 그 정도로도 동작한다. 제품에서는 곧 문제가 나온다. 누가 방에 들어올 수 있는지 확인해야 하고, offer와 answer 순서가 꼬이지 않아야 하며, candidate가 remote description보다 먼저 도착했을 때 버퍼링해야 한다. 재접속한 peer가 예전 상태와 섞이지 않게 정리하는 일도 필요하다.
signaling 설계의 핵심은 메시지 전달 속도보다 순서, 권한, 재접속 처리다. signaling을 구현하며 가장 피하고 싶은 상태는 조용한 불일치다. 한쪽 브라우저는 answer를 보냈다고 믿고, 다른 쪽은 아직 offer 대기 상태에 있으며, 서버 로그에는 단순 전달 성공만 남는 상황이다. 이때 사용자는 연결 실패를 보고하지만 운영자는 볼 근거가 없다. 메시지 수신과 처리 성공을 구분해서 남겨야 한다.
서버가 전달했다는 사실과 클라이언트가 상태에 반영했다는 사실은 다르다. 재협상은 데모에서 자주 빠지는 부분이다. 화면 공유를 켜거나 카메라를 바꾸거나 네트워크가 바뀌면 renegotiation이 필요할 수 있다. 이때 offer 충돌을 어떻게 처리할지 정하지 않으면 어느 한쪽 브라우저에서는 되는 것처럼 보이고 다른 쪽에서는 멈춘다. polite peer와 impolite peer 같은 규칙을 두거나 서버가 순서를 제한하는 방식으로 충돌을 줄여야 한다.
그래서 signaling 서버는 소개자이면서 기록자에 가깝다. 실제 파일이나 미디어 내용을 듣지 않아도 누가 방에 있었는지, 어떤 peer가 어떤 상태였는지, 연결 약속이 어느 단계까지 갔는지는 알아야 한다. 이때도 권한과 데이터 경로를 섞으면 안 된다. 방 입장 권한은 서버가 확인하고, 확인된 뒤에 SDP와 candidate를 전달한다. payload가 나중에 피어 사이로 흐르더라도 약속을 무작정 통과시켜서는 안 된다.
signaling 메시지는 작지만 신뢰할 수 없는 네트워크를 지난다. 클라이언트가 메시지를 중복으로 보낼 수 있고, 늦게 도착할 수 있고, 이미 닫힌 세션의 candidate가 뒤늦게 들어올 수도 있다. 그래서 메시지에는 세션을 구분할 수 있는 값이 필요하다. 방 ID만으로 부족할 때가 많다. peer ID, connection attempt ID, 메시지 순서나 생성 시각을 같이 두면 오래된 메시지를 버릴 근거가 생긴다.
WebRTC를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
프로토콜 관점에서 본 signaling
signaling 서버를 만들 때 WebSocket을 고르는 이유도 다시 봐야 한다. WebSocket은 양방향 이벤트가 잦은 연결 약속 교환에 편하다. 하지만 WebSocket을 썼다는 사실이 signaling 설계를 보장하지는 않는다. 인증 갱신, 방 나가기, 브라우저 탭 종료, 네트워크 끊김, 재접속 시 peer 정리 같은 일이 빠지면 프로토콜은 실시간이어도 상태는 불안정하다. 중요한 것은 전송 수단보다 세션 수명과 메시지 순서다. signaling은 데이터 경로를 대신하지 않지만 데이터 경로가 시작되게 만드는 약속 통로다.
signaling을 공부하며 가장 많이 바뀐 생각은 “연결 전 단계”라는 표현이었다. signaling은 연결 전에만 잠깐 쓰는 부품이 아니다. 재접속, 재협상, 권한 변경, 방 퇴장, 네트워크 전환 때마다 다시 등장한다. WebRTC 세션의 수명 전체를 따라다니는 제어면이라고 보는 쪽이 더 정확하다. 이 관점으로 보면 작은 signaling 서버도 제품의 핵심 서버가 된다. 권한 확인도 단발성으로 끝나지 않는다. 사용자가 방에 들어올 때는 권한이 있었지만 전송 직전에 만료될 수 있고, 관리자가 링크를 폐기할 수도 있다. signaling 서버가 단순 중계만 하면 이런 변화가 반영되지 않는다. 민감한 기능에서는 SDP나 candidate를 전달하기 전마다 최소한의 권한 상태를 다시 확인하거나, 세션 토큰의 수명을 짧게 잡아야 한다. 연결 약속도 제품 정책 안에서 움직여야 한다.
브라우저가 주는 제약
P2P에서 sign 이렇게 적어 두면 다음 구현에서 같은 질문을 다시 꺼내기 쉽다. 용어보다 책임, 성공보다 실패, 구조보다 사용자가 보는 상태를 먼저 확인하는 습관이 남는다. Signaling을 검토할 때는 메시지가 작다고 가볍게 보지 않는다. 실제 바이트를 운반하지 않아도 방 권한, 초대 상태, 후보 전달, 만료 정책이 모두 이 경로에 걸린다. signaling이 흔들리면 직접 연결은 시작조차 못 한다.
그래서 화면에는 “상대와 연결 정보를 교환하는 중” 같은 짧은 문장을 두고, 로그에는 어떤 약속이 누락됐는지 남겨야 한다. 내가 남길 기준은 네 가지다. offer와 answer 순서를 상태 머신으로 제한한다. candidate는 remote description 이후 처리하고, 먼저 도착한 것은 검증해 버퍼링한다. 방 권한 확인과 signaling 전달을 분리하지 않는다. 재접속할 때 이전 peer 상태와 새 peer 상태를 명확히 갈라 준다.
여기에 로그 이름과 사용자 문구를 붙이면 장애 대응이 훨씬 덜 막힌다. PonsLink와 PonsWarp에서도 이 문제는 다른 이름으로 나타난다. 방과 요청 상태는 중앙에서 일관성 있게 관리해야 했고, PonsWarp의 signaling-rs는 직접 파일 전송이 시작되기 전 연결 약속을 질서 있게 전달해야 했다. 데이터는 가능하면 직접 보내고 싶어도 연결 약속은 서버가 책임지는 편이 안전했다.
이 지점에서 signaling은 P2P의 반대편이 아니라 P2P를 가능하게 하는 중앙 책임이다. 상태 머신을 먼저 적으면 이런 문제를 줄일 수 있다. 방 생성, 권한 확인, peer 등록, offer 생성 가능 상태, answer 대기, candidate 버퍼링, connected, disconnected, reconnecting, closed를 나눠 둔다. 각 상태에서 받을 수 있는 메시지와 버릴 메시지를 정한다. 특히 candidate는 remote description 설정 이후 처리하고, 그 전에 온 candidate는 peer와 세션이 맞는지 확인한 뒤 임시로 쌓아 둔다. '[WebRTC 딥다이브 P2P에서 signaling 서버는 왜 필요한가'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
설계할 때 던져야 할 질문
사용자 문구는 signaling의 내부 상태를 그대로 드러내지 않아도 된다. “상대와 연결 정보를 맞추는 중이에요”, “상대가 다시 접속해 연결을 정리하고 있어요”, “방 권한을 확인하는 중이에요” 정도면 충분하다. 운영자 로그에는 offer_collision, candidate_buffered_before_remote_description, peer_rejoined, unauthorized_sdp_blocked처럼 더 정확한 이름이 필요하다. 화면과 로그가 같은 사건을 가리켜야 문의를 빠르게 따라갈 수 있다. signaling 서버가 scale out될 때도 생각할 거리가 생긴다.
같은 방의 메시지가 여러 서버 인스턴스에 흩어지면 순서와 전달 보장이 흔들릴 수 있다. Redis pub/sub이나 message broker를 쓰더라도 방 단위 ordering, presence 정리, disconnect 감지를 어떻게 할지 정해야 한다. WebRTC 연결은 브라우저끼리 만들어지지만, 그 연결을 시작하는 signaling은 서버 운영의 영향을 그대로 받는다. 다음에는 topology를 봐야 한다. signaling이 약속을 잡아도 Mesh로 연결할지, SFU를 둘지, MCU를 쓸지에 따라 비용과 실패 모양이 완전히 달라진다.
P2P에서 서버가 사라지지 않는다는 첫 노트의 결론은 signaling에서 더 분명해진다. 서버는 payload를 직접 나르지 않아도 연결의 질서를 책임질 수 있다. 실험할 때는 메시지 순서를 일부러 흔들어 본다. answer보다 candidate가 먼저 오게 만들고, 두 탭이 동시에 offer를 보내게 하고, 한쪽이 새로고침한 뒤 예전 candidate가 늦게 도착하게 둔다. 좋은 네트워크에서 한 번 연결되는지보다 이런 어긋난 순서를 어떻게 정리하는지가 signaling 품질을 가른다.
제품의 signaling은 빠른 중계기보다 안전한 상태 관리자에 가깝다. signaling에서 흔한 실패는 의외로 사소한 순서 문제에서 시작한다. 양쪽이 동시에 offer를 만들면 glare가 생긴다. candidate가 너무 빨리 도착하면 아직 remote description이 없어 처리할 수 없다. 사용자가 새로고침한 뒤 같은 방에 다시 들어오면 이전 peer ID와 새 peer ID가 섞일 수 있다. 권한 확인이 끝나기 전에 SDP를 전달하면 방 정책과 연결 상태가 어긋난다.
로그가 없으면 이 모든 일이 그냥 “연결 안 됨”으로 보인다. signaling은 화려한 기술이 아니다. 대부분의 시간에는 작은 JSON 메시지를 옮길 뿐이다. 하지만 그 작은 메시지가 연결의 출발선이고, 권한의 경계이며, 나중에 장애를 해석할 단서다. WebRTC를 이해한다는 것은 미디어나 DataChannel API만 보는 일이 아니라 이 약속 교환을 제품의 상태로 다루는 일까지 포함한다.
이어서 보면 좋은 글
결국 signaling은 “WebRTC 밖의 일”이 아니라 WebRTC 제품의 절반이다. 브라우저 API가 offer와 answer의 형태를 알려 줄 뿐, 그 메시지가 누구에게 언제 전달되고 어떤 정책으로 막히는지는 서비스가 책임진다. realtime-netwo signaling은 payload를 나르는 길이 아니라 연결 전후의 약속을 주고받는 제어면이다. offer, answer, ICE candidate, 방 상태, peer ID, 권한 확인, 재협상 이벤트가 여기로 지나간다. WebSocket을 많이 쓰지만 필수는 아니다. HTTP polling이나 서버 이벤트, 자체 broker로도 만들 수 있다.
중요한 것은 방식보다 순서와 권한, 상태를 잃지 않는 일이다. signaling 구현에서 또 하나 조심할 것은 메시지 스키마다. 데모에서는 {type, payload} 정도로 시작해도 되지만 제품에서는 버전, roomId, peerId, attemptId, createdAt, traceId 같은 값이 필요해진다. 스키마가 없으면 클라이언트 버전이 섞였을 때 어떤 메시지를 받아야 하는지 판단하기 어렵다. 특히 모바일이나 데스크톱 앱처럼 업데이트 속도가 다른 클라이언트가 있다면 호환성 규칙을 먼저 둬야 한다.
작은 signaling 서버라도 이 원칙을 지키면 제품의 신뢰도가 달라진다. 메시지를 빨리 전달하는 것보다 맞는 상대에게 맞는 순서로 전달하는 것이 먼저다. 그다음 실패했을 때 어떤 단계에서 멈췄는지 볼 수 있어야 한다. 이 두 가지가 있으면 WebRTC 문제의 절반은 이미 좁혀진다. 특히 WebRTC에서는 재현 조건을 같이 적어야 의미가 있었다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.