junyeokk
Blog
Network·2026. 02. 15

WebSocket 프로토콜

HTTP는 요청-응답 모델이다. 클라이언트가 요청을 보내야만 서버가 응답할 수 있다. 서버가 먼저 클라이언트에게 데이터를 보내는 건 불가능하다. 채팅이나 실시간 알림처럼 서버가 능동적으로 데이터를 푸시해야 하는 상황에서 이 구조는 근본적으로 맞지 않는다.

HTTP 위에서 실시간을 흉내내는 방법들이 있었다. 폴링(polling)은 클라이언트가 일정 간격으로 계속 요청을 보내는 방식이다. 변경이 없어도 요청이 나간다. 롱 폴링(long polling)은 서버가 새 데이터가 있을 때까지 응답을 보류하는 방식인데, 커넥션이 유지되는 동안 서버 자원을 점유하고, 응답 후 다시 연결해야 하는 오버헤드가 있다.

WebSocket은 이 문제를 프로토콜 레벨에서 해결한다. HTTP 커넥션을 업그레이드해서 양방향 통신 채널을 만들고, 이후부터는 HTTP 없이 데이터를 주고받는다.


핸드셰이크

WebSocket 연결은 HTTP에서 시작한다. 클라이언트가 일반 HTTP 요청에 업그레이드 헤더를 붙여서 보낸다.

text
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

핵심은 Upgrade: websocket이다. "이 HTTP 연결을 WebSocket으로 전환하고 싶다"는 뜻이다. Sec-WebSocket-Key는 클라이언트가 생성한 랜덤 값으로, 서버가 정상적인 WebSocket 서버인지 확인하는 데 쓰인다.

서버가 수락하면 101 상태 코드로 응답한다.

text
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept는 클라이언트가 보낸 Key에 고정 문자열(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)을 붙이고 SHA-1 해시한 뒤 Base64 인코딩한 값이다. 클라이언트는 이 값을 검증해서 상대가 진짜 WebSocket을 이해하는 서버인지 확인한다. 보안이 아니라 프로토콜 준수 확인 용도다.

이 핸드셰이크가 끝나면 TCP 연결은 그대로 유지되고, 프로토콜만 WebSocket으로 바뀐다. 이후 HTTP 헤더 없이 프레임 단위로 데이터가 오간다.


프레임 구조

WebSocket은 메시지를 프레임(frame) 단위로 전송한다. HTTP처럼 매번 헤더를 붙이는 게 아니라, 최소 2바이트짜리 경량 헤더만 사용한다.

text
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (0 or 4 bytes)      |                         |
+-------------------------------------+                         |
|                    Payload Data                               |
+---------------------------------------------------------------+

주요 필드를 보면:

  • FIN: 이 프레임이 메시지의 마지막 조각인지 표시. 큰 메시지는 여러 프레임으로 나눠 보낼 수 있다.
  • opcode: 프레임 종류. 0x1이면 텍스트, 0x2면 바이너리, 0x8이면 연결 종료, 0x9/0xA는 ping/pong.
  • MASK: 클라이언트→서버 프레임은 반드시 마스킹해야 한다. 서버→클라이언트는 마스킹하지 않는다.
  • Payload length: 125 이하면 그 자체가 길이, 126이면 다음 2바이트가 길이, 127이면 다음 8바이트가 길이.

마스킹은 보안이 아니라 프록시 캐시 오염 공격을 방지하기 위한 것이다. 중간 프록시가 WebSocket 프레임을 HTTP로 오인해서 캐시에 저장하는 걸 막는다.


연결 유지와 종료

WebSocket 연결은 한번 맺으면 명시적으로 닫을 때까지 유지된다. 연결이 살아있는지 확인하기 위해 ping/pong 프레임을 사용한다. 한쪽이 ping(opcode 0x9)을 보내면, 상대는 반드시 동일한 payload로 pong(opcode 0xA)을 응답해야 한다.

연결을 닫을 때는 close 프레임(opcode 0x8)으로 정상 종료한다. TCP의 FIN처럼 양쪽이 주고받는 구조다.

text
클라이언트 → 서버: Close (status: 1000, reason: "Normal closure")
서버 → 클라이언트: Close (status: 1000)
// 이후 TCP 연결 종료

상태 코드 1000은 정상 종료, 1001은 서버 종료/페이지 이동, 1006은 비정상 종료(close 프레임 없이 끊김)다.


HTTP와의 차이

HTTPWebSocket
방향요청-응답 (단방향)양방향 (full-duplex)
연결매 요청마다 새로 (또는 keep-alive)한번 연결 후 유지
오버헤드매 요청에 헤더 포함핸드셰이크 이후 최소 2바이트
프로토콜HTTP/1.1, HTTP/2ws://, wss://
적합한 용도문서 조회, API 호출실시간 채팅, 게임, 주식 시세

WebSocket이 만능은 아니다. 단순 데이터 조회에는 HTTP가 낫고, 서버 인프라가 long-lived 커넥션을 감당할 수 있어야 한다. SSE(Server-Sent Events)처럼 서버→클라이언트 단방향만 필요한 경우에는 더 가벼운 대안도 있다.


브라우저에서의 사용

javascript
const ws = new WebSocket('wss://example.com/chat');

ws.onopen = () => {
  console.log('연결됨');
  ws.send('hello');
};

ws.onmessage = (event) => {
  console.log('받은 데이터:', event.data);
};

ws.onclose = (event) => {
  console.log(`종료: ${event.code} ${event.reason}`);
};

ws.onerror = (error) => {
  console.error('에러 발생');
};

send()로 텍스트와 바이너리(ArrayBuffer, Blob) 모두 보낼 수 있다. readyState 프로퍼티로 현재 연결 상태를 확인할 수 있는데, 0(CONNECTING), 1(OPEN), 2(CLOSING), 3(CLOSED) 네 가지다.


핵심 정리

WebSocket은 HTTP의 요청-응답 한계를 넘어서 양방향 실시간 통신을 가능하게 하는 프로토콜이다. HTTP로 핸드셰이크한 뒤 TCP 연결을 그대로 재활용하고, 경량 프레임 구조로 오버헤드를 최소화한다. 폴링이나 롱 폴링의 비효율 없이 서버가 원할 때 클라이언트에게 데이터를 푸시할 수 있다는 게 핵심이다.

관련 문서