안동민 개발노트 아이콘

안동민 개발노트

12장 : HTTP/2, HTTP/3, WebSocket

WebSocket과 실시간 통신

HTTP는 기본적으로 클라이언트가 요청을 열고 서버가 응답을 돌려주는 모델입니다. 서버가 브라우저 쪽에 임의의 순간 새 연결을 열어 데이터를 밀어 넣는 구조가 아니기 때문에, 채팅, 실시간 알림, 주식 시세처럼 지연이 짧은 양방향 통신에서는 단순한 요청-응답만으로 한계가 드러납니다.


HTTP로 실시간을 흉내내는 방법들

짧은 주기로 계속 물어보는 polling은 구현이 쉽지만 빈 응답과 HTTP 헤더 비용이 큽니다. Long polling은 서버가 새 이벤트가 생길 때까지 응답을 늦춰 낭비를 줄이지만, 응답이 끝날 때마다 다시 요청을 열어야 합니다. SSE는 서버에서 클라이언트로 흐르는 이벤트 스트림에 잘 맞고, WebSocket은 한 번 연결한 뒤 같은 연결에서 양쪽이 독립적으로 메시지를 보낼 수 있습니다.


WebSocket 핸드셰이크

브라우저의 WebSocket 연결은 보통 HTTP/1.1 Upgrade 메커니즘으로 시작합니다. wss://를 쓰면 이 핸드셰이크도 TLS 연결 위에서 이뤄집니다.

Sec-WebSocket-Accept는 클라이언트가 보낸 키에 매직 문자열 258EAFA5-E914-47DA-95CA-C5AB0DC85B11을 결합하고 SHA-1 해시를 취한 뒤 Base64로 인코딩한 값입니다. 이 값은 서버가 WebSocket 핸드셰이크를 이해했는지 확인하기 위한 장치이지, 사용자 인증이나 암호화 기능은 아닙니다.

HTTP/2에서는 일반적인 Upgrade 헤더 대신 RFC 8441의 Extended CONNECT 방식으로 WebSocket을 시작할 수 있습니다. 다만 클라이언트, 서버, 프록시, 로드 밸런서가 모두 지원해야 하므로 실무에서는 인프라 지원 여부를 확인해야 합니다.


WebSocket 프레임 구조

WebSocket은 메시지를 하나 이상의 프레임으로 나눠 보낼 수 있습니다. 텍스트와 바이너리 프레임은 애플리케이션 데이터를 담고, close, ping, pong 같은 제어 프레임은 연결 상태를 관리합니다. 브라우저가 서버로 보내는 프레임은 마스킹이 필수이며, 이는 중간 장비 오동작을 줄이기 위한 프로토콜 장치이지 암호화가 아닙니다.

Opcode프레임 유형용도
0x0Continuation이전 프레임의 계속
0x1TextUTF-8 텍스트 메시지
0x2Binary바이너리 데이터
0x8Close연결 종료
0x9Ping연결 상태 확인
0xAPongPing에 대한 응답

WebSocket 구현

websocket_server.py
import asyncio
import websockets

connected = set()

async def handler(websocket):
    connected.add(websocket)
    try:
        async for message in websocket:
            # 받은 메시지를 모든 클라이언트에게 브로드캐스트
            for ws in connected:
                if ws != websocket:
                    await ws.send(message)
    finally:
        connected.discard(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8080):
        print("WebSocket server on ws://0.0.0.0:8080")
        await asyncio.Future()  # 서버 유지

asyncio.run(main())
websocket_client.js
const ws = new WebSocket("ws://localhost:8080");

ws.onopen = () => {
  console.log("Connected");
  ws.send("Hello!");
};

ws.onmessage = (event) => {
  console.log("Received:", event.data);
};

ws.onclose = () => {
  console.log("Disconnected");
};

ws.onerror = (error) => {
  console.error("Error:", error);
};

SSE (Server-Sent Events)

모든 실시간 통신에 WebSocket이 필요한 것은 아닙니다. SSE(Server-Sent Events)는 HTTP 응답을 text/event-stream으로 유지하면서 서버에서 클라이언트로 이벤트를 흘려보냅니다.

SSE는 브라우저의 EventSource가 재연결과 Last-Event-ID 처리를 도와주기 때문에 알림, 피드, 진행률 표시처럼 서버발 이벤트가 대부분인 화면에 적합합니다. 반대로 클라이언트도 높은 빈도로 메시지를 보내야 하거나 바이너리 데이터를 주고받아야 한다면 WebSocket이 더 자연스럽습니다.

항목WebSocketSSELong Polling
방향양방향서버→클라이언트주로 서버→클라이언트
연결 방식ws:// 또는 wss://HTTP 응답 스트림반복 HTTP 요청
자동 재연결직접 구현브라우저 내장직접 구현
바이너리지원텍스트 이벤트 중심HTTP 응답 형식에 따름
HTTP/2/3인프라 지원 필요일반 응답처럼 동작일반 요청처럼 동작
프록시 영향설정 영향 큼비교적 예측 가능비교적 예측 가능
적합한 사례채팅, 게임, 협업알림, 피드, 진행률레거시 호환

Socket.IO의 역할

Socket.IO는 단순한 WebSocket wrapper라기보다 자체 클라이언트와 서버 프로토콜을 가진 실시간 통신 라이브러리입니다. WebSocket을 사용할 수 있으면 WebSocket을 쓰고, 환경에 따라 HTTP long polling을 fallback으로 사용할 수 있습니다.

Socket.IO는 재연결, heartbeat, 방(room), namespace, acknowledgement 같은 기능을 제공하지만, 순수 WebSocket 클라이언트와 그대로 호환되는 프로토콜은 아닙니다. 양쪽 모두 Socket.IO를 쓰는 애플리케이션인지, 아니면 표준 WebSocket API만 필요한지 먼저 구분해야 합니다.

다음 장에서는 코드를 넘어 실무 네트워크 인프라 — CDN, 로드 밸런서, 프록시, 클라우드 네트워크 — 를 다루겠습니다.