업데이트됨 1개월 전
HTTP는 인간의 대화 방식과 근본적으로 맞지 않습니다. 누군가 먼저 말을 걸어야만 대답할 수 있으니까요.
HTTP에서는 클라이언트가 요청하면 서버가 응답합니다. 그리고 침묵. 서버가 새로 전할 것이 생겨도—채팅 메시지가 도착했거나, 주가가 바뀌었거나, 다른 플레이어가 움직였거나—기다려야 합니다. 서버가 먼저 연락할 수 없습니다. 요청을 받았을 때만 응답할 수 있습니다.
이것이 실시간 통신 문제입니다.
차선책들 — 그리고 왜 통하지 않는가
**폴링(Polling)**은 힘으로 밀어붙이는 방식입니다. 몇 초마다 변화가 있는지 묻는 것이죠. "새 메시지 있어?" "아니." "새 메시지 있어?" "아니." "새 메시지 있어?" "응, 여기 있어." 이 방식은 대역폭을 낭비하고, 서버 자원을 소모하고, 지연을 만들어냅니다. 폴링 주기가 될 때에야 업데이트를 알 수 있으니까요.
**롱 폴링(Long polling)**은 좀 더 영리합니다. 서버가 무언가 일어날 때까지 요청을 붙잡고 있다가 응답합니다. 클라이언트는 즉시 또 다른 요청을 보냅니다. 항상 하나의 요청이 대기 중입니다. 하지만 여전히 요청-응답 사이클을 반복하며, 메시지마다 오버헤드가 생깁니다.
Server-Sent Events는 서버가 클라이언트에게 푸시할 수 있게 해줍니다—하지만 서버에서 클라이언트 방향만 됩니다. 클라이언트가 무언가 보내야 한다면, 다시 일반 HTTP 요청으로 돌아가야 합니다. 대화가 필요한 세상에서의 일방통행입니다.
WebSockets는 지속적이고 양방향의 연결을 수립하여 이 문제를 해결합니다. 어느 쪽이든 원할 때 메시지를 보낼 수 있습니다. 허락을 구할 필요 없습니다. 차례를 기다릴 필요도 없습니다.
핸드셰이크: HTTP가 자신의 후계자를 부트스트랩하다
WebSockets는 우아한 묘수를 씁니다. HTTP를 사용해서 HTTP가 아닌 것을 만들어냅니다.
클라이언트는 평범한 HTTP 요청처럼 보이지만 특별한 헤더가 달린 것을 보냅니다:
서버가 응답합니다:
"101 Switching Protocols"—연결이 중간에 변환됩니다. HTTP를 말하던 동일한 TCP 연결이 이제 WebSocket을 말합니다. HTTP 대화는 끝났고, 새로운 프로토콜이 자리를 차지합니다.
이것은 실용적인 천재성입니다. WebSockets는 기존 HTTP 인프라를 타고 방화벽과 프록시를 통과한 다음, 안으로 들어오면 그 껍데기를 벗어버립니다.
전송 형식
WebSocket 메시지는 프레임 단위로 전달됩니다—작은 헤더(2~14바이트)에 페이로드가 뒤따릅니다.
텍스트 프레임은 UTF-8 문자열을 담습니다. JSON이 여기에 속합니다.
바이너리 프레임은 원시 바이트를 담습니다. 이미지, 오디오, 바이너리 프로토콜이 여기에 속합니다.
제어 프레임은 연결 자체를 관리합니다. Ping은 "살아 있나요?"라고 묻고, Pong은 "네"라고 답합니다. Close는 정상적인 종료를 시작합니다.
**마스킹(Masking)**은 클라이언트에서 서버로 가는 메시지에 대한 보안 요구사항입니다. 캐시 포이즈닝 공격을 막기 위해 페이로드를 무작위 마스크로 XOR 처리합니다. 서버에서 클라이언트로 가는 메시지는 이를 생략합니다—서버는 신뢰할 수 있으니까요.
대용량 메시지는 여러 프레임에 걸쳐 분할될 수 있어, 하나의 대규모 전송이 긴급한 작은 메시지를 막는 것을 방지합니다.
WebSockets가 가능하게 하는 것들
채팅이 가장 대표적인 예입니다. 메시지가 도착하는 즉시 서버가 푸시하기 때문에 바로 나타납니다—폴링 지연도, 낭비되는 요청도 없습니다.
협업 편집은 Google Docs나 Figma에서 볼 수 있습니다. 모든 키 입력, 모든 커서 움직임이 사용자들 사이에서 실시간으로 동기화됩니다.
게임은 낮은 지연의 양방향 통신이 필요합니다. 내 행동이 빠르게 서버에 전달되고, 다른 플레이어의 움직임도 빠르게 나에게 전달됩니다.
실시간 대시보드는 지표, 상태, 알림을 발생하는 즉시 보여줍니다. 데이터가 여러분에게 푸시됩니다.
트레이딩 플랫폼에서는 밀리초가 중요합니다. 가격 업데이트가 지속적으로 스트리밍되고, 주문이 즉시 확인됩니다.
알림은 생성되는 즉시 전달됩니다. 다음 폴링 때까지 기다리지 않습니다.
클라이언트 측 API
JavaScript의 WebSocket API는 놀랍도록 단순합니다:
이벤트 네 개. send 메서드 하나. close 메서드 하나. 전체 인터페이스가 이게 전부입니다.
서버 측 현실
모든 주요 언어에 WebSocket 라이브러리가 있습니다:
- Node.js: ws (간결한), socket.io (기능이 풍부한), uWebSockets (성능 중심)
- Python: websockets, aiohttp, Django Channels
- Go: gorilla/websocket
- Java: javax.websocket, Spring WebSocket
- Ruby: ActionCable
서버는 핸드셰이크를 처리하고, 연결된 클라이언트를 추적하고, 메시지를 라우팅하고, 연결 끊김을 정리합니다.
메시지 패턴
브로드캐스트: 모두에게 전송. 채팅 메시지는 방의 모든 멤버에게 전달됩니다.
타깃 전송: 한 명에게 전송. 개인 메시지는 수신자에게만 전달됩니다.
방/채널: 클라이언트를 논리적으로 그룹화. 모든 사람 대신 "room-123"에 브로드캐스트합니다.
요청-응답: RPC를 그 위에 얹습니다. 메시지 ID를 포함하고, ID로 응답을 매칭합니다.
Pub/Sub: 클라이언트가 토픽을 구독하고, 요청한 것만 받습니다.
확장성 문제
WebSockets는 상태를 갖습니다(stateful). 각 연결은 메모리를 차지하고, 파일 디스크립터를 소비하고, 컨텍스트를 유지합니다. 이것은 확장을 쉽게 만들었던 HTTP의 아름다운 무상태성(statelessness)을 깨뜨립니다.
사용자 A가 서버 1에 연결되고 사용자 B가 서버 2에 연결된다면, A의 메시지는 어떻게 B에게 전달될까요?
메시지 브로커가 이 문제를 해결합니다. Redis Pub/Sub, RabbitMQ, 또는 Kafka가 서버들 사이에 위치합니다. 서버 1이 발행하면, 서버 2가 구독하고 전달합니다. 모든 서버가 브로커를 통해 다른 모든 서버와 통신합니다.
**스티키 세션(Sticky sessions)**은 사용자를 같은 서버에 유지시켜 라우팅을 단순화하지만, 특정 서버에 부하가 집중되는 문제를 만들 수 있습니다.
연결 한도는 중요합니다. 인프라가 처리할 수 있는 동시 연결 수를 파악하세요. 그에 맞게 계획하세요.
보안
인증은 핸드셰이크 시 이루어집니다(쿠키가 작동합니다). 하지만 권한 부여는 모든 메시지마다 이루어져야 합니다. 클라이언트는 거짓말을 합니다.
입력 유효성 검사는 반드시 해야 합니다. 모든 메시지는 신뢰할 수 없는 입력입니다.
**속도 제한(Rate limiting)**은 스팸과 남용을 방지합니다.
메시지 크기 제한은 메모리 고갈을 방지합니다.
**WSS (WebSocket Secure)**는 프로덕션에서 필수입니다. HTTPS를 보호하는 것과 같은 TLS입니다.
**핸드셰이크 시 출처 검증(Origin verification)**은 크로스 사이트 WebSocket 하이재킹을 방지합니다.
연결이 끊어질 때
끊어지게 됩니다. 네트워크는 실패합니다. 서버는 재시작됩니다. 모바일 기기는 잠듭니다.
**하트비트(Heartbeat)**는 죽은 연결을 감지합니다. Pong 응답이 없다면? 연결이 사라진 것입니다.
자동 재연결은 지수 백오프(exponential backoff)와 함께 사용하세요. 힘겨워하는 서버를 계속 두드리지 마세요.
메시지 ID 또는 시퀀스 번호로 누락을 감지합니다. 잃어버린 것을 재전송합니다.
재개 프로토콜은 짧은 연결 끊김 이후 클라이언트가 놓친 메시지를 따라잡을 수 있게 합니다.
WebSockets를 쓰지 말아야 할 때
Server-Sent Events는 서버에서 클라이언트로의 단방향 스트리밍을 더 단순하게 처리합니다. 클라이언트가 수신만 한다면 SSE를 고려하세요.
일반 HTTP는 요청-응답 패턴에 잘 작동합니다. 실시간이 필요 없는 작업에 WebSocket의 복잡성을 추가하지 마세요.
WebTransport(HTTP/3 기반)는 WebSocket과 유사한 기능을 더 나은 성능으로 제공하는, 새 애플리케이션의 미래입니다.
WebSockets는 양쪽이 모두 말해야 할 때, 지연이 중요할 때, 실시간이 선택이 아닐 때 빛을 발합니다. 서버가 마침내 먼저 대화를 시작할 수 있게 해주는 프로토콜입니다.
WebSockets에 관해 자주 묻는 질문
WebSockets는 방화벽과 프록시를 통과합니까?
대부분 그렇습니다. 핸드셰이크가 HTTP처럼 보여서 대부분의 인프라가 통과시킵니다. 제한적인 네트워크에서는 WS(포트 80)보다 WSS(포트 443)가 더 잘 작동합니다. 일반 HTTPS 트래픽처럼 보이기 때문입니다.
서버는 몇 개의 WebSocket 연결을 처리할 수 있습니까?
서버와 각 연결이 하는 일에 따라 다릅니다. 잘 조정된 서버는 대부분 유휴 상태인 연결을 수만 개에서 수십만 개까지 처리할 수 있습니다. 많은 메시지 처리를 하는 활성 연결은 더 빨리 한계에 도달합니다. 일반적으로 메모리와 파일 디스크립터가 제약 요인입니다.
WebSockets를 사용해야 할까요, socket.io를 사용해야 할까요?
socket.io는 WebSockets 위에 기능을 추가합니다: 자동 재연결, 방 관리, 구형 브라우저 폴백, 수신 확인(acknowledgments). 이런 기능들이 필요하다면 socket.io가 작업을 줄여줍니다. 최소한의 오버헤드와 제어를 원한다면 순수 WebSockets가 더 단순합니다.
WebSockets가 REST API를 대체할 수 있습니까?
할 수는 있지만, 보통 그러면 안 됩니다. REST의 무상태성은 캐싱, 확장, 디버깅을 더 쉽게 만듭니다. 일반적인 CRUD 작업에는 REST를 사용하고, 실시간 기능에는 WebSockets를 사용하세요. 많은 애플리케이션이 둘 다 사용합니다.
클라이언트 메시지에 마스킹이 필요한 이유는 무엇입니까?
캐시 포이즈닝 공격을 방지하기 위해서입니다. 마스킹이 없다면 악의적인 클라이언트가 중간 프록시에 유효한 HTTP 응답처럼 보이는 WebSocket 프레임을 만들 수 있어, 캐시를 오염시킬 가능성이 있습니다. 무작위 마스크가 이를 실용적으로 불가능하게 만듭니다.
이 페이지가 도움이 되었나요?