WebSocket API 완전 이해

WebSocket 연결 수립(HTTP Upgrade), readyState 상태 머신, send()와 바이너리 전송, bufferedAmount 흐름 제어, 지수 백오프 재연결, 서브프로토콜, Heartbeat 패턴까지 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 SSE로 서버에서 클라이언트로 단방향 스트림을 구성하는 방법을 살펴봤습니다. 이번에는 양방향 전이중 통신을 제공하는 WebSocket API를 정리합니다. 실시간 채팅, 협업 도구, 멀티플레이어 게임, 금융 틱 피드에 사용되는 프로토콜입니다.


WebSocket이란

WebSocket은 HTTP Upgrade 핸드셰이크를 거쳐 확립되는 지속적인 TCP 연결입니다. 한 번 연결되면 서버와 클라이언트가 동시에 양방향으로 메시지를 주고받을 수 있습니다. HTTP의 요청-응답 모델과 달리 서버가 먼저 메시지를 보낼 수 있습니다.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <hash>

이 핸드셰이크 이후 TCP 연결은 WebSocket 프레임 형식으로 전환됩니다.


연결 수립과 readyState

WebSocket 연결 상태 머신

const ws = new WebSocket('wss://api.example.com/ws');
// wss:// = TLS 위 WebSocket (프로덕션 필수)
// ws://  = 평문 (로컬 개발용)

console.log(ws.readyState); // 0: CONNECTING

ws.onopen = (event) => {
  console.log(ws.readyState); // 1: OPEN
  ws.send(JSON.stringify({ type: 'hello' }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log('수신:', msg);
};

ws.onerror = (event) => {
  // 에러는 항상 onclose 직전에 발생
  console.error('WebSocket error');
};

ws.onclose = (event) => {
  console.log(ws.readyState); // 3: CLOSED
  console.log('code:', event.code, 'reason:', event.reason);
};

readyState 값: CONNECTING(0)OPEN(1)CLOSING(2)CLOSED(3). OPEN 상태에서만 send()가 작동합니다.


메시지 전송

// 텍스트 (JSON 직렬화)
ws.send(JSON.stringify({ type: 'chat', text: '안녕' }));

// ArrayBuffer (바이너리)
const buffer = new ArrayBuffer(4);
new DataView(buffer).setInt32(0, 42);
ws.send(buffer);

// Blob (파일 전송)
const file = fileInput.files[0];
ws.send(file);

// 수신 측 바이너리 타입 설정 (기본값: 'blob')
ws.binaryType = 'arraybuffer'; // 또는 'blob'
ws.onmessage = ({ data }) => {
  if (data instanceof ArrayBuffer) {
    const view = new DataView(data);
    console.log(view.getInt32(0));
  }
};

bufferedAmount — 흐름 제어

WebSocket 실전 패턴

ws.bufferedAmount는 아직 전송되지 않은 큐의 바이트 수를 나타냅니다. 높은 빈도로 send()를 호출하면 메모리가 무한정 늘어날 수 있습니다.

function throttledSend(ws, data) {
  if (ws.readyState !== WebSocket.OPEN) return;
  if (ws.bufferedAmount > 16 * 1024) {
    // 16KB 이상 쌓여 있으면 전송 건너뜀
    return;
  }
  ws.send(data);
}

// 게임 상태 60fps 전송 예시
setInterval(() => {
  throttledSend(ws, JSON.stringify(getGameState()));
}, 1000 / 60);

지수 백오프 재연결

WebSocket은 SSE와 달리 자동 재연결이 없습니다. 직접 구현해야 합니다.

class ReconnectingWebSocket {
  #url;
  #delay = 1000;
  #ws = null;

  constructor(url) {
    this.#url = url;
    this.#connect();
  }

  #connect() {
    this.#ws = new WebSocket(this.#url);
    this.#ws.onopen = () => {
      this.#delay = 1000; // 재연결 성공 시 초기화
      this.onopen?.();
    };
    this.#ws.onmessage = (e) => this.onmessage?.(e);
    this.#ws.onclose = ({ code }) => {
      if (code === 1000) return; // 정상 종료는 재연결 안 함
      this.onclose?.();
      setTimeout(() => this.#connect(), this.#delay);
      this.#delay = Math.min(this.#delay * 2, 30_000); // 최대 30초
    };
  }

  send(data) {
    if (this.#ws?.readyState === WebSocket.OPEN) {
      this.#ws.send(data);
    }
  }

  close() { this.#ws?.close(1000, 'user closed'); }
}

CloseEvent.code === 1000은 정상 종료이므로 재연결하지 않습니다. 네트워크 문제(1006)나 서버 종료(1001)는 재연결이 필요합니다.


Heartbeat (Ping/Pong)

프록시나 방화벽이 유휴 WebSocket 연결을 끊을 수 있습니다. 주기적으로 Ping 메시지를 보내면 연결을 살아있게 유지합니다.

const HEARTBEAT_INTERVAL = 25_000; // 25초

ws.onopen = () => {
  const heartbeat = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    } else {
      clearInterval(heartbeat);
    }
  }, HEARTBEAT_INTERVAL);
};

ws.onmessage = ({ data }) => {
  const msg = JSON.parse(data);
  if (msg.type === 'pong') return; // Heartbeat 응답은 무시
  handleMessage(msg);
};

브라우저 WebSocket API는 프로토콜 레벨 Ping/Pong 프레임을 직접 보낼 수 없으므로, 애플리케이션 레벨 메시지로 구현합니다.


서브프로토콜

// 클라이언트에서 서브프로토콜 협상
const ws = new WebSocket('wss://example.com/ws', ['json-rpc', 'graphql-ws']);
ws.onopen = () => {
  console.log('협상된 프로토콜:', ws.protocol); // 'json-rpc' 또는 서버 선택값
};

서버는 Sec-WebSocket-Protocol 헤더로 지원하는 프로토콜 중 하나를 선택해 응답합니다. GraphQL 구독에는 graphql-ws, JSON-RPC에는 jsonrpc 서브프로토콜이 표준화되어 있습니다.


보안 고려사항

wss:// 사용: 평문 ws://는 중간자 공격에 취약합니다. 프로덕션에서는 반드시 TLS를 사용하세요.

Origin 검증: 서버 측에서 Origin 헤더를 검증해 CSRF를 방지합니다.

메시지 크기 제한: 서버에서 수신 메시지 크기에 상한을 설정하세요. 대용량 메시지는 DoS 벡터가 될 수 있습니다.

인증: 핸드셰이크 시 쿠키 또는 URL 파라미터로 토큰을 전달합니다. 연결 후 첫 메시지로 인증 토큰을 보내는 패턴도 널리 사용됩니다.


SSE vs WebSocket 정리

항목SSEWebSocket
자동 재연결브라우저 내장직접 구현
서버 부담HTTP와 동일지속 연결 관리
바이너리불가가능
HTTP/2 멀티플렉싱가능불가
클라이언트→서버fetch로 별도동일 연결

채팅, 게임, 협업처럼 양방향 저지연이 필요하면 WebSocket, 서버 알림·피드처럼 단방향으로 충분하면 SSE를 선택하세요.


지난 글: Server-Sent Events · EventSource 완전 이해

다음 글: WebRTC 개요 · P2P 실시간 통신


읽어주셔서 감사합니다. 😊