실시간 웹의 진화 — 폴링에서 웹소켓과 WebRTC까지의 패러다임
월드 와이드 웹(WWW)의 근간을 이루는 HTTP/1.1 프로토콜은 본질적으로 ‘문서를 요청하고 받아오는 단방향 비연결형(Stateless & Request-Response)’ 모델로 설계되었습니다. 클라이언트가 먼저 말을 걸지 않으면 서버는 응답할 수 없으며, 요청에 대한 응답이 끝나면 TCP 연결은 끊어지거나 유휴 상태로 돌아갔습니다. 신문 기사나 학술 논문을 읽는 하이퍼텍스트 환경에서는 완벽한 아키텍처였습니다.
그러나 주식 거래소의 호가창, 수천 명이 동시에 떠드는 실시간 채팅, 여러 사용자가 같은 캔버스에 그림을 그리는 협업 도구의 시대가 열리면서 이 단방향 모델은 거대한 병목으로 변했습니다. 서버에 언제 새로운 데이터가 도착할지 모르는 상황에서 클라이언트는 어떻게 1초 미만의 지연 시간으로 최신 상태를 받아올 수 있는가? 지난 20년간 웹 엔지니어링의 역사는 이 질문에 답하기 위해 HTTP의 구조적 족쇄를 풀고 실시간성을 쟁취해 온 처절한 패러다임 진화의 여정이었습니다.
1. 숏 폴링(Short Polling): 가장 단순하지만 가장 파괴적인 낭비
실시간성을 흉내 내기 위해 개발자들이 가장 먼저 시도한 방식은 주기적 요청(Short Polling) 이었습니다.
클라이언트 브라우저가 setInterval 타이머를 켜두고 1초, 2초, 5초마다 서버를 향해 “새로운 메시지 있나요?”라는 HTTP GET 요청을 기계적으로 쏘아 올리는 구조입니다.
sequenceDiagram
autonumber
actor Client as Client Browser
participant Server as Web Server
Client->>Server: GET /api/messages (Check for updates)
Server-->>Client: 200 OK [] (No new data)
Note over Client,Server: Wait interval (e.g., 2 seconds)
Client->>Server: GET /api/messages (Check for updates)
Server-->>Client: 200 OK [] (No new data)
Note over Server: New event occurs on server!
Client->>Server: GET /api/messages (Check for updates)
Server-->>Client: 200 OK [New Message!]
숏 폴링은 구현이 극단적으로 쉽다는 장점이 있었지만, 대규모 서비스에서는 서버와 네트워크 대역폭을 파괴하는 재앙이었습니다.
첫째는 빈 응답(Empty Response)의 낭비 입니다. 10만 명의 사용자가 1초마다 폴링을 보내는데 서버에 전달할 새 메시지가 없다면, 서버는 초당 10만 번의 무의미한 데이터베이스 쿼리를 실행하고 빈 배열 []을 반환해야 합니다.
둘째는 HTTP 헤더의 오버헤드 입니다. 전달하려는 데이터는 불과 몇 바이트의 텍스트 한 줄이거나 심지어 0바이트인데도, 매 요청마다 User-Agent, Cookie, Accept 등 약 500바이트에서 1KB에 달하는 HTTP 요청/응답 헤더가 네트워크를 가득 채웠습니다. 실시간성을 흉내 내는 대가로 전송 데이터의 99%가 쓰레기 헤더 패킷이었던 셈입니다.
2. 롱 폴링(Long Polling): 연결을 붙잡고 버티는 침묵의 트릭
숏 폴링의 극단적인 자원 낭비를 줄이기 위해 고안된 기법이 바로 롱 폴링(Long Polling) , 일명 코멧(Comet) 아키텍처였습니다.
롱 폴링의 핵심은 “데이터가 없으면 서버가 바로 응답하지 않고 연결을 붙잡아 둔다”는 점입니다.
sequenceDiagram
autonumber
actor Client as Client Browser
participant Server as Web Server
Client->>Server: GET /api/messages (Pending connection)
Note over Server: Server holds connection open<br/>Waiting for events...
Note over Server: New event occurs on server!
Server-->>Client: 200 OK [New Message!]
Note over Client: Immediately reconnects
Client->>Server: GET /api/messages (Pending connection)
Note over Server: Server holds connection open...
- 클라이언트가 서버로 HTTP 요청을 보냅니다.
- 서버는 즉시 응답하지 않고, 해당 HTTP 연결을 보류(Pending) 상태로 대기시킵니다.
- 다른 사용자로부터 새 메시지가 서버에 도착하면, 보류하고 있던 클라이언트의 HTTP 연결을 통해 즉시 데이터를 쏘아주고 연결을 종료합니다.
- 데이터를 수신한 클라이언트는 지체 없이 곧바로 다음 HTTP 요청을 다시 날려 또다시 서버에 연결을 걸어둡니다.
만약 일정 시간(예: 30초–60초) 동안 아무런 이벤트가 발생하지 않으면 연결은 타임아웃으로 종료되고, 클라이언트는 즉시 재연결 요청을 보냅니다.
롱 폴링은 빈 패킷 요청 횟수를 획기적으로 줄여주었고 데이터 발생 즉시 전달할 수 있었지만, 여전히 근본적인 한계를 안고 있었습니다. 데이터가 한 번 도착할 때마다 HTTP 트랜잭션이 종료되므로, 메시지를 받을 때마다 새로운 TCP 3-way 핸드셰이크와 TLS 암호화 협상을 매번 다시 수행 해야 했습니다. 트래픽이 몰려 초당 수십 건의 메시지가 오가는 방에서는 결국 숏 폴링과 다를 바 없는 극심한 부하가 발생했습니다.
3. SSE(Server-Sent Events): 단방향 스트리밍의 우아한 표준
롱 폴링처럼 억지로 연결을 끊고 맺기를 반복하지 않고, 하나의 단일 HTTP 연결을 닫지 않은 채 계속 열어두고 서버에서 일방적으로 데이터를 밀어넣을 수 없을까? 이 아이디어가 표준화된 것이 HTML5의 SSE(Server-Sent Events) 입니다.
SSE는 전통적인 HTTP/1.1의 파이프라인 위에서 동작하지만, 응답 헤더에 특별한 MIME 타입을 선언합니다.
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked
data: {"user": "alice", "text": "Hello World"}
data: {"user": "bob", "text": "Nice to meet you"}
클라이언트는 표준 자바스크립트 API인 EventSource를 사용해 단 몇 줄로 연결할 수 있습니다.
// Native browser EventSource API
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Streamed from server:", data);
};
sequenceDiagram
autonumber
actor Client as Client Browser
participant Server as Web Server
Client->>Server: GET /api/stream (Accept: text/event-stream)
Server-->>Client: 200 OK (Keep-Alive, Stream open)
Note over Client,Server: Single persistent HTTP connection kept open
Note over Server: New event 1 occurs!
Server-->>Client: data: {"text": "Hello"}
Note over Server: New event 2 occurs!
Server-->>Client: data: {"text": "World"}
Note over Client: Upstream data requires separate HTTP POST
Client->>Server: POST /api/messages (Independent HTTP request)
Server-->>Client: 200 OK (Saved)
SSE는 완벽한 장점들을 가졌습니다.
- HTTP 친화적 : 별도의 프로토콜 전환 없이 기존 HTTP 인프라(방화벽, 프록시, L7 로드밸런서)를 그대로 통과합니다.
- 자동 재연결(Reconnection) : 연결이 끊기면 브라우저가 알아서 마지막 이벤트 ID(
Last-Event-ID)를 들고 재연결을 시도합니다. - 단순함 : 복잡한 소켓 프로그래밍 없이 단방향 푸시를 구현할 수 있습니다.
그러나 치명적인 제약이 있었습니다. SSE는 철저하게 서버에서 클라이언트로만 흐르는 ‘단방향(Half-Duplex)’ 통신 입니다. 클라이언트가 서버로 메시지를 보내려면 완전히 별개의 HTTP POST 요청을 날려야 했습니다. 또한 UTF-8 텍스트 데이터만 전송할 수 있어 바이너리 스트리밍에 적합하지 않았습니다.
주식 시세, 알림 피드, LLM의 토큰 스트리밍(ChatGPT 응답 화면)에는 완벽한 도구였지만, 양방향으로 고속 대화를 주고받아야 하는 대화형 서비스에는 반쪽짜리 해결책이었습니다.
4. 웹소켓(WebSocket): HTTP의 껍질을 벗고 전이중(Full-Duplex)으로
2011년 IETF RFC 6455로 표준화된 웹소켓(WebSocket) 은 웹에서 실시간성을 구현하는 패러다임을 영구히 바꾸어 놓았습니다.
웹소켓은 HTTP의 한계를 우회하려 꼼수를 부리는 대신, “처음 연결할 때만 HTTP의 문을 두드리고, 연결이 수립되는 순간 HTTP의 껍질을 완전히 벗어던지는” 파격적인 방식을 채택했습니다.
프로토콜 업그레이드 핸드셰이크 (HTTP 101 Switching Protocols)
클라이언트는 웹소켓 전용 포트를 새로 여는 대신, 기존 웹 서버가 수신하고 있는 80(HTTP) 또는 443(HTTPS) 포트로 정규 HTTP GET 요청을 보내면서 헤더에 프로토콜 업그레이드를 요구합니다.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
서버가 웹소켓을 지원한다면, 101 Switching Protocols 응답 코드로 화답합니다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이 핸드셰이크가 성공적으로 완료되는 즉시, 클라이언트와 서버 사이의 통신은 HTTP 파서(Parser)를 완전히 바이패스 합니다. 그 순간부터 양 끝단은 TCP 소켓의 양방향 파이프라인을 온전히 장악하며, 양방향 전이중(Full-Duplex) 통신 모드로 진입합니다.
sequenceDiagram
autonumber
actor Client as Client Browser
participant Server as Web Server
Note over Client,Server: 1. HTTP Protocol Upgrade Handshake
Client->>Server: GET /chat (Upgrade: websocket, Sec-WebSocket-Key)
Server-->>Client: 101 Switching Protocols (Connection: Upgrade)
Note over Client,Server: 2. Full-Duplex Persistent TCP Framing
Client->>Server: Binary/Text Frame (Min 2–6 bytes header)
Server->>Client: Binary/Text Frame (Min 2 bytes header)
Server->>Client: Server Push Event (Instant)
Client->>Server: Client Send Event (Instant)
2바이트의 기적: 초경량 바이너리 프레이밍
웹소켓이 가져온 가장 거대한 혁신은 헤더 오버헤드의 극단적 절감 입니다.
매번 1KB에 달하는 텍스트 헤더를 실어나르던 HTTP와 달리, 웹소켓의 메시지 프레임(Frame) 헤더는 최소 2바이트(Masking 포함 시 클라이언트 기준 6바이트) 에 불과합니다.
flowchart TD
subgraph Header["RFC 6455 WebSocket Frame Header (2–14 Bytes)"]
direction LR
B0["<b>Byte 0: Flags & Opcode</b><br/>FIN (1b) | RSV1-3 (3b)<br/>Opcode (4b: Text, Binary, Ping)"]
B1["<b>Byte 1: Mask & Len</b><br/>MASK (1b)<br/>Payload Len (7b: 0–125)"]
B2["<b>Bytes 2–9: Extended Len</b><br/>16-bit (if len=126)<br/>64-bit (if len=127)"]
B3["<b>Bytes 2–13: Masking Key</b><br/>32-bit Masking Key<br/>(Client to Server only)"]
end
subgraph Payload["Application Data"]
DATA["<b>Payload Data</b><br/>Unmasked (Server to Client) / Masked (Client to Server) Application Message"]
end
Header --> Payload
단 2바이트의 비트 필드 안에 메시지의 완결 여부(FIN 비트), 데이터 타입(텍스트, 바이너리, Ping, Pong 등 Opcode), 페이로드 길이, 마스킹 키가 완벽하게 압축되어 있습니다.
클라이언트는 서버의 요청을 기다릴 필요 없이 언제든 즉시 프레임을 쏘아 보낼 수 있고, 서버 역시 아무런 지연 없이 연결된 소켓으로 데이터를 뱉어냅니다. 현대 웹 애플리케이션의 실시간 협업, 주식 호가, 대규모 채팅 시스템은 사실상 전부 이 웹소켓 프로토콜 위에서 숨 쉬고 있습니다.
5. WebRTC: 서버를 거치지 않는 브라우저 간 P2P 통신
웹소켓이 클라이언트와 서버 사이의 통신을 완성했다면, WebRTC(Web Real-Time Communication) 는 한 걸음 더 나아가 “중앙 서버조차 거치지 않고 브라우저와 브라우저가 직접(Peer-to-Peer) 데이터를 주고받는” 종착지에 도달했습니다.
웹소켓 환경에서 A가 B에게 메시지를 보내면 반드시 중앙 서버를 거쳐야 하므로, 서버의 지리적 위치와 네트워크 트래픽에 따라 최소 수십 밀리초의 레이턴시가 발생합니다.
반면 WebRTC의 DataChannel 은 시그널링(연결을 위한 IP/포트 교환) 단계에서만 서버를 이용하고, 연결이 성립된 이후에는 두 브라우저 간에 직접 암호화된 UDP(SCTP) 터널 을 뚫어 데이터를 쏩니다.
flowchart LR
subgraph WS["WebSocket: Central Server Relay"]
direction LR
C1["Browser A"] -->|"TCP (ws/wss)"| S["Central Server"]
S -->|"TCP (ws/wss)"| C2["Browser B"]
end
flowchart LR
subgraph RTC["WebRTC: Direct Peer-to-Peer"]
direction LR
C1["Browser A"] <-->|"Direct UDP/SCTP Channel"| C2["Browser B"]
C1 -.->|"Signaling"| SIG["Signaling Server"]
SIG -.->|"Signaling"| C2
end
화상 회의(Google Meet), 실시간 음성 통신(Discord 음성), 1:1 대용량 파일 전송, 클라우드 게이밍처럼 밀리초 단위의 초저지연(Ultra-low latency)이 생명인 영역은 이제 웹소켓을 넘어 WebRTC P2P로 넘어가고 있습니다.
6. 프로토콜별 특성과 엔지니어링 트레이드오프
실시간성을 제공하는 4가지 기술은 어느 하나가 다른 하나를 완벽히 대체하는 상위 호환의 관계가 아닙니다. 각각의 기술은 인프라 복잡도, 유지 비용, 그리고 통신의 특성에 따라 뚜렷한 트레이드오프를 가지고 있습니다.
| 기술 | 통신 방향 | 기본 프로토콜 | 헤더 오버헤드 | 연결 유지 방식 | 주 사용처 |
|---|---|---|---|---|---|
| Short Polling | 단방향 (Pull) | HTTP | 매우 큼 (약 1KB) | 비연결 (주기적 재연결) | 변경 빈도가 극히 낮은 배치 상태 조회 |
| Long Polling | 유사 양방향 | HTTP | 큼 (약 1KB) | 이벤트 발생 시 단절 후 재연결 | 구형 브라우저 호환, 단순 푸시 |
| SSE | 단방향 (Server Push) | HTTP | 작음 (최초 1회 헤더) | 단일 HTTP 연결 지속 유지 | 뉴스 피드, 주식 시세, LLM 텍스트 스트리밍 |
| WebSocket | 양방향 전이중 (Full-Duplex) | TCP (ws/wss) | 극소 (2–6 Bytes) | 영구 TCP 연결 지속 유지 | 실시간 다자간 채팅, 동시 편집, 금융 주문 |
| WebRTC | 양방향 P2P (Full-Duplex) | UDP (SCTP/SRTP) | 극소 (P2P 패킷) | 직접 연결 (NAT 홀펀칭 터널) | 화상 회의, 원격 제어, P2P 파일 전송 |
시스템 아키텍처를 설계할 때 가장 경계해야 할 함정은 “실시간 기능이 필요하니 무조건 웹소켓을 붙이자”는 접근입니다.
단순히 알림 배지 카운트나 공지사항 팝업을 띄우는 기능에 웹소켓을 도입하면, 수십만 대의 단말이 쥐고 있는 유휴 소켓 커넥션을 관리하느라 서버 메모리와 로드밸런서의 파일 디스크립터가 고갈되는 낭비가 발생합니다. 단순히 서버에서 텍스트를 밀어주기만 하면 되는 작업에는 HTTP 인프라의 캐싱과 로드밸런싱을 그대로 누릴 수 있는 SSE가 훨씬 비용 효율적입니다.
결국 실시간 통신 엔지니어링의 본질은 가장 화려한 최신 프로토콜을 선택하는 것이 아니라, 서비스가 요구하는 지연 시간의 한계와 트래픽의 방향성, 그리고 서버가 감당해야 할 커넥션 비용 사이에서 가장 균형 잡힌 프로토콜을 낙점하는 것 입니다.