tech

브라우저만으로 내 게임을?
WebRTC 리모트플레이 서버 구축기

1. 프롤로그 – 우연한 합류, 그리고 첫걸음

예전에는 모니터 앞에 각 잡고 앉아 키보드와 마우스로 밤을 지새우곤 했다. 하지만 어느 순간부터 게임 플레이 방식이 달라졌다. 퇴근 후에는 책상보다 소파가 더 편했고 컨트롤러 하나를 쥐고 짬나는 대로 이어서 즐기는 것이 일상이 됐다. 자연스럽게 PS5 Remote Play나 Xbox 클라우드 게이밍 같은 스트리밍 플레이가 익숙해졌다. 플레이 공간이 바뀌니 필요한 기술도 달라진 것이다.

그때 사내에서 ‘리모트플레이’를 개발하는 팀이 새로 꾸려진다는 소식을 들었다. 내가 요즘 즐기는 그 경험을 우리 플랫폼에서도 구현한다는 소식에 반가움이 밀려왔다. 처음에는 개발을 맡은 분들과 이런저런 이야기를 나누며 어떤 기술로 구현할지 설명을 들었고 관심은 점점 커져갔다. 그러던 중 우연히 합류 제안을 받았고 그렇게 팀에 함께하게 됐다.

문제는 핵심 기술이 WebRTC라는 점이었다. 한 번도 다뤄본 적 없는 분야였다. 그럼에도 내가 원하는 플레이 방식—어디서든 편한 자리에서 바로 이어 즐기는 경험—을 직접 구현할 수 있다는 기대가 더 컸다. 그렇게 나의 첫 리모트플레이 개발 여정이 시작됐다.

2. WebRTC 기술 이해 – 개념과 흐름 잡기

2.1 어떻게 배웠나: 문서 -> 샘플 -> 가설 검증

합류 직후 가장 먼저 한 일은 개념을 정립하는 것이었다. Google Developers, MDN, RFC, WebRTC 샘플 Repository를 차례로 훑으며 용어를 정리한 뒤, 바로 실습에 들어갔다.

2.1.1 Offer / Answer 감 잡기

두 개의 브라우저 창을 열어 공식 샘플로 Offer/Answer를 직접 교환하고, 카메라 영상을 양쪽에서 송·수신하며 샘플링하고 흐름을 익혔다.

2.1.2 STUN / TURN 체감

STUN만 설정한 상태에서 사무실 ↔ 모바일 환경을 테스트하니 연결 실패가 잦았다. TURN을 추가하자 안정적으로 연결되는 것을 확인했고, TURN의 가치를 확실히 이해할 수 있었다.

2.2 주요 용어 모아보기

용어설명
WebRTC브라우저 기반 실시간 미디어, 데이터 전송 기술
시그널링연결 정보를 교환해 협상 하는 과정 (프로토콜 자유)
SDP코덱, 암호화, 전송 방식 등 세션 정보
ICE가능한 연결 경로 후보를 찾고 시험하는 절차
STUNNAT 환경에서 공인 IP / Port 를 알아내는 프로토콜
TURNP2P가 불가능할때 서버를 경유해 미디어 전송
CandidateICE가 찾은 연결 후보(IP, Port, 프로토콜)
Trickle ICE후보를 발견 즉시 전송, 연결 시간을 단축

2.3 연결 과정 흐름

2.3 연결 과정 흐름

이 과정을 완전히 이해한 순간, WebRTC는 단순한 ‘연결’ 기술이 아니라 네트워크라는 미지의 공간에서 최적의 경로를 탐험해 나가는 여정에 가깝다는 것을 깨달았다.

3. 시그널링 서버 구축 – 만남을 주선하다

“잘 들리십니까?… 미래에서 온 무전입니다.”


드라마 시그널의 장면처럼, 우리의 시그널링도 두 피어가 만나기 전 조건과 좌표(SDP, Candidate)를 무전하듯 주고받는 것으로 시작한다. 여기서 시그널링 서버는 무전기와 같다. 한쪽에서 보낸 메시지가 다른 쪽에 안전하게 그리고 지연 없이 도착해야 한다.

무전기의 성패는 결국 얼마나 빠르게 신호를 전달하느냐에 달려 있었다. 후보는 두가지였다. Redis Pub/Sub과 Apache Kafka.

3.1 Redis VS Kafka – 그 긴 고민의 밤  

구분RedisKafka
지연시간매우 짧음짧음
메시지 보존없음(Pub/Sub, 오프라인 재전송 미지원)있음 (로그 기반, 리플레이 가능)
확장성중간매우 높음
구축 난이도낮음높음
적합 환경실시간, 일회성 신호 전달대규모 로그, 분석, 장기보관

Kafka의 내구성과 확장성은 매력적이었다. 하지만 시그널링은 지속 보관과 리플레이가 필요 없는 즉시성 신호가 핵심이었다. 우리는 낮은 지연과 단순한 운영을 위해 Redis Pub/Sub을 선택했다.

우리가 필요로 한 건 저지연 브로드캐스트였고 오프라인 구독자에게 과거 메시지를 재전송할 요구사항이 없었다. 실제로 메시지를 못 받는 경우는 장애(네트워크/프로세스 다운) 이거나 채널의 구독 상태가 잘못된 경우였고, 이를 줄이기 위해 몇가지 규칙을 정했다.

Kafka의 내구성과 확장성은 매력적이었지만 시그널링의 본질은 즉시성이었다. 지속 보관과 리플레이가 필요 없었고 오히려 낮은 지연과 단순한 운영이 더 중요했다. 그래서 우리는 Redis Pub/Sub을 선택했다.

우리가 필요로 한 건 저지연 브로드캐스트였으며 오프라인 구독자에게 과거 메시지를 재전송할 요구사항은 없었다. 실제 메시지 누락은 네트워크 장애나 프로세스 다운 혹은 잘못된 채널 구독 상태에서 발생했다. 이를 줄이기 위해 다음과 같은 규칙을 마련했다.

  1. 채널 명명 규칙 고정 & 구독 전 검증
  2. RoomId 기반 구독 상태 체크 후, Publish (구독자 수 확인 기능 추가)
  3. 간단한 수신 확인으로 운영 안전망 확보

문제는 Redis 자체가 아니라 지속성이 없는 Pub/Sub의 특성이었다. 우리는 구독 상태 확인과 재시도 설계로 운영상의 누락을 방지했다.

Note.
실제 운영에서는 발행 전 구독자 수를 매번 확인하지 않았다. 핵심은 속도였으며, 누락 가능성은 상위 로직(Trickle ICE 반복 전송, 재연결 시 최신 SDP 재전송)과 알림 체계로 보완했다.

3.2 메시지 프로토콜 – 무전기의 언어

무전에도 약속된 코드가 있듯 시그널링 메시지에도 공용 언어(JSON)가 필요했다. 웹과 Windows 클라이언트 모두 동일한 규격으로 대화해야 서로를 정확히 인식하고 이해할 수 있다.

메시지 타입 예시

  1. Offer: 위치 정보 송신 -> SDP Offer
  2. Answer: 확인, 경로 확보 -> SDP Answer
  3. Candidate: 새 경로 발견 -> ICE 후보
  4. join / leave: 방 참가/퇴장
  5. error: 오류 신호

메시지 구조 예시

{
    "type": "candidate",
    "roomId": "abcd1234",
    "payload": {
      "candidate": "candidate:842163049 1 udp 1677729535 192.168…",
      "sdpMid": "0",
      "sdpMLineIndex": 0
    }
}

처리 흐름 – 무전 릴레이

  1. Peer A -> Websocket -> 시그널링 서버(송신)
  2. 서버는 RoomId로 대상 Peer 라우팅
  3. Redis Pub/Sub으로 다른 시그널링 인스턴스에 전파
  4. 대상 Peer로 포워딩(수신)

3.3 레이턴시 확인 – 타임스탬프 한 줄

복잡한 분산 트레이싱까지 필요하지 않았다.

보내기 전에 타임스탬프 하나(ts_sent) 넣고 받은 쪽에서 계산하여 느리면 알림을 전송 했다.

복잡한 분산 트레이싱은 필요하지 않았다. 메시지를 보내기 전 타임스탬프 하나(ts_sent)를 넣고 받은 쪽에서 계산하여 느리면 알림을 주는 방식이면 충분했다.

  1. 보내는 쪽: 메시지에 ts_sent 추가
  2. 받는 쪽: latency = now() – ts_sent 계산
  3. 알림: latency가 기준(예: 200ms) 넘으면 운영 알림 채널로 전송

간단하지만 효과적이었다. 언제 어디서 지연이 발생하는지 빠르게 파악할 수 있었고, 필요할 때만 원인을 깊이 추적하면 됐다.

4. TURN 서버 구축 – 실패를 대비한 우회로

현실의 네트워크는 이상과 다르다. 기업 방화벽, 공공망, 일부 해외망에서는 P2P 연결이 원활히 성립되지 않는다. 이때 TURN은 마지막 안전망이다. 우리는 Coturn을 선택했다.

4.1 왜 Coturn 인가 – 선택 배경

  1. 성숙도 & 실전 검증: 업계에서 가장 널리 쓰이는 오픈소스 STUN/TURN 서버 중 하나로 레퍼런스가 풍부하고, 문제 발생 시 해결 사례를 쉽게 찾을 수 있다.
  2. 표준 충실도: STUN / TURN / ICE 관련 RFC를 충실히 구현해 브라우저 간 상호운용성이 높다. UDP / TCP / TLS까지 기본 제공.
  3. 간단한 운영: 패키지와 Docker 모두 간편 배포가 가능하며 설정 파일 기반이라 운영 난이도가 낮다.
  4. 인증 유연성: 고정 계정(lt-cred)부터 공유 시크릿 기반 시간제한 자격증명까지 지원한다.
  5. IPv4 / IPv6 & 정책 제어: v4/v6 동시 지원, 포트 범위 제한, 대역폭 상한, 타임아웃 등 비용·품질 정책 적용이 용이하다.
  6. 문제 분리: 서비스와 독립 구성으로 장애 격리가 쉽고, 리전 단위 수평 확장에 유리하다.
Note.
내부 기준은 “빠르게 붙여보고, 문제 없으면 그대로 가져간다”였다. 테스트에서 최소 설정만으로 바로 연결을 띄우고 정책과 보안만 보강하는 방식으로 진입 장벽을 낮췄다.

4.1 Coturn의 표준 준수와 호환성

Coturn은 널리 쓰이는 오픈소스 STUN/TURN 서버로, WebRTC가 의존하는 핵심 표준을 충실히 구현했다. 표준 기반이기 때문에 브라우저, 플랫폼 간 상호운용성이 높고, 설정만으로 다양한 네트워크 제약에 대응할 수 있다.

  1. STUN: RFC 5389 – NAT 뒤 클라이언트가 자신의 공인 IP/Port를 알아내는 절차
  2. TURN: RFC 5766 – P2P 가 불가능할 때 Relay 경로를 제공(UDP 기본)
  3. TURN over TCP/TLS: RFC 6062 – UDP 가 불가능한 환경을 위한 TCP/TLS Relay
  4. ICE: RFC 8445 – STUN/TURN 후보를 수집 및 검증하여 최적의 경로를 선택하는 알고리즘
  5. NAT 특성 확인: RFC 5780 – NAT 동작 성향을 추정(환경 진단에 유용)

Coturn은 WebRTC 의존 핵심 표준을 충실히 구현해 브라우저·플랫폼 간 호환성이 높다.

  • STUN: RFC 5389 – NAT 뒤 클라이언트가 자신의 공인 IP/Port를 알아내는 절차
  • TURN: RFC 5766 – P2P가 불가능할 때 Relay 경로 제공(UDP 기본)
  • TURN over TCP/TLS: RFC 6062 – UDP 불가 환경에서 TCP/TLS Relay 지원
  • ICE: RFC 8445 – STUN/TURN 후보를 수집·검증하여 최적 경로 선택
  • NAT 특성 확인: RFC 5780 – NAT 동작 성향 추정(환경 진단에 유용)

4.2 핵심 설

  1. 프로토콜: UDP 우선, 불가 시 TCP Fallback(RFC 6062)
  2. 보안/인증: TLS over TCP, Long-Term Credential 또는 공유 시크릿 기반 일회용 자격증명
  3. 운영: 리전 분산 + GeoIP로 최단 거점 선택, 포트 범위 제한
  4. 정책: Relay 타임아웃/대역폭 상한과 접속 원천 제한 등 비용 제어

4.3 Coturn(STUN/TURN) 적용과 흐름도

Coturn 적용과 흐름도

4.4 운영 팁(간단 테스트 & 진단)

  1. turnutils_uclient / turnutils_peer(coturn 제공 도구)로 STUN/TURN 경로를 빠르게 점검
  2. 클라이언트 로그에서 ICE 후보 타입(host /srflx / relay) 비율을 확인해 Relay 의존도 추적
  3. Relay 트래픽 급증은 비용과 품질 모두에 영향 → 모니터링 임계치 알림과 함께 원인을 바로 확인
Note.
국내 모바일 환경처럼 IPv6 보급이 낮고 NAT이 보편적인 구간에서 TURN 의존도가 높아지기 쉽다. 그래서 우리 서비스에서 TURN을 항상 가동 중인 필수 인프라로 두고 정책 및 모니터링으로 비용과 품질을 관리해야 한다.

5. 모니터링 환경 구축 – 보이지 않는 무선 품질

5.1 구성

  실시간 서비스의 안정성은 관측에서 시작된다. 우리는 Prometheus, Grafana, AlertManager 조합으로 모니터링과 알림을 구성했다.

항목설명
Prometheus시그널링, TURN, Redis, 호스트 리소스 메트릭 수집
Grafana대시보드(연결 성공률, P2P/TURN 비율, 평균 연결 시간, RTT, Relay 트래픽, 시그널링 지연)
AlertManager(알림 전송)메트릭 수집 실패, 릴레이 비율 급증, CPU/메모리 임계치 초과 시그널링 지연 지연 > 200 ms 지속 시 운영 채널로 즉시 통지

5.2 커스텀 지표 & 효과

  1. 시그널링 실시간 접속자(소켓) / 협상 시간 분포 / ICE 후보 교환(타임스탬프 기반)
  2. TURN 릴레이 비율, 프로토콜(UDP/TCP) 비중 / 세션별 대역폭 추이

  이 조합으로 특정 시간대에 릴레이 급증이나 특정 리전의 성공률 저하를 분 단위로 감지하고 대응할 수 있었다. 

6. 시스템 점검 – JMeter로 시그널링 흔들어 보기

처음에는 미디어까지 합성해 테스트할까 고민했지만 브라우저 렌더링/코덱/드라이버 변수까지 한번에 섞으면 원인 분석이 어려워진다. 그래서 원인 분석 난이도를 고려해 컨트롤 플레인(WebSocket 시그널링)에 집중했다. 목표는 ‘연결을 많이 만들고, 자주 협상시키고, 일부러 흔들어도 안정성을 유지하는지’ 확인하는 것이었다.

6.1 대상과 방법

  1. WebSocket 연결, 유지, 복구
    1. Handshake, ping/pong keep-alive, 세션 지속과 자동 재연결 확인
  2. SDP/ICE 메시지 전파
    1. Offer / Answer ICE Candidate가 시그널링 ↔ Redis Pub/Sub ↔ 상대 피어로 정상 라우팅 되는지 체크.
    2. 전송 직전 메시지에 ts_sent만 심고, 수신 측에서 now() – ts_sent로 왕복 지연을 구해 임계 초과 시 알림.
  3. 부하 모델
    1. 동시 WebSocket 연결 수를 1,000 → 10,000까지 단계적으로 증가 후 장시간 유지해 CPU, 메모리, 지연이 안정적인지 확인.
    2. 수 ms ~ 수십 ms 간격으로 Offer / Answer 후보 연속 전송 → 순간 메시지 폭주 상황을 의도적으로 재현

6.2 결과와 해석

 동시접속 1만 명을 유지한 상태에서도 시그널링 인스턴스의 CPU와 메모리는 눈에 띄는 변동이 거의 없었다. 장시간 유지구간에서도 스파이크나 누적 증가 없이 리소스 측면의 여유가 확인됐다. WebSocket 세션은 네트워크 흔들림 상황에서도 잘 붙어 있었고, 재협상도 문제없이 정상적으로 반복됐다. 시그널링 왕복 지연은 우리가 잡은 기준선 안에서 안정적으로 유지 됐고 리소스의 알림 임계치를 넘는 구간은 없었다. Redis Pub/Sub 전파 지연도, 수 ms 대에 고정되어 브로커 병목 신호가 관측되지 않았다. 

즉, 시그널링은 처리량, 회복력, 지연 측면에서 기대 이상으로 안정적이었다. 

Note.
쓰레드를 과하게 올리면 로컬에서 먼저 문제가 발생한다. 한 번은 개발 PC에 블루스크린까지 발생했다. 그래서 테스트 전용 PC 6대로 분산 실행해 인프라 안정성을 확보했다. 간단해 보여도 테스트 인프라가 버텨야 서버 신호가 제대로 보인다.

7. 에필로그 – 첫 전송의 순간

 문서와 로그의 낯선 용어를 지나 수십 번의 실패를 통과하고 나서야 브라우저에 첫 프레임이 떴다. P2P가 열리면 직진하고, 막히면 TURN을 통해 우회 하는 흐름이 우리가 만든 시그널링과 릴레이 위에서 흔들림 없이 동작했다. 아직은 할 일이 많다. 품질 최적화, 전 세계 거점 확장, 더 촘촘한 모니터링.

하지만 한 가지는 분명하다. 어디서든 브라우저만 있다면 게임은 계속된다. 그리고 그걸 가능하게 한 건, ‘미래에서 온 무전’처럼 서로의 조건을 주고 받던 그 작은 시그널에서 시작됐다. 돌아보면 내가 개발을 시작한 순간도 이런 작은 시그널에서 비롯된 것일지도 모르겠다. 그 신호가 길을 열어줬고 다음 길도 그럴 것이라 믿는다. 이 작은 시그널을 잊지 않겠다. 앞으로도 그 신호를 놓치지 않고 한 걸음씩 더 나아가야겠다.

김남식 기자

작은 시그널에서 시작한 기록입니다. 누군가의 다음 연결을 조금이라도 빠르게 만드는 것에 도움이 되길 바랍니다.


TOP