C.W.K.
Stream
Lesson 04 of 05 · published

WebSocket — 영속적 양방향 연결

~12 min · io-net, websocket, realtime

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"HTTP 는 request/response. WebSocket 은 전화 통화. 밑은 같은 TCP, 대화 모양은 완전 달라."

WebSocket 이 푸는 문제

HTTP 는 "클라이언트가 묻고, 서버가 답하고, 대화 끝" 에 아름답게 작동. "서버가 예측 불가능한 순간에 클라이언트한테 뭔가 push 하고 싶음" 엔 나쁘게 작동. 역사적 우회책 — long-polling, server-sent events — 다 단점 (오버헤드, 단방향만, 재연결 churn).

WebSocket 이 이거 위해 설계된 프로토콜: 일회성 HTTP-기반 handshake 후, 같은 TCP 소켓이 양방향 메시지-패싱 채널 됨. 어느 쪽이든 언제든 메시지 보낼 수 있어. 프레이밍은 작아 (메시지당 몇 바이트 오버헤드). Connection 이 양쪽이 필요한 만큼 열려 있어.

Native ws 스토리 (그리고 왜 라이브러리 쓰는지)

Node 가 node:ws *클라이언트* 출하 — Node 22+ 에 브라우저 API 매치하는 클라이언트 사이드 connection 위한 글로벌 WebSocket 클래스 있음:

const ws = new WebSocket('wss://echo.example.com');
ws.addEventListener('open', () => ws.send('hello'));
ws.addEventListener('message', (ev) => console.log(ev.data));
ws.addEventListener('close', () => console.log('done'));

*서버* 쪽엔 Node 가 내장 안 출하. De facto 라이브러리는 ws (npm: ws) — battle-tested, Socket.IO 가 밑에서 씀, LSP 에코시스템이, Next.js dev 서버가. cwkPippa 가 Cinder 브리지에 씀. 재발명 마.

최소 서버

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 7070 });

wss.on('connection', (socket, req) => {
  console.log('client connected from', req.socket.remoteAddress);

  socket.on('message', (data) => {
    // Broadcast to everyone
    for (const client of wss.clients) {
      if (client.readyState === 1) client.send(data.toString());
    }
  });

  socket.on('close', () => console.log('client gone'));
});

이게 작동하는 chat 서버. 모든 클라이언트가 연결, 모든 메시지가 모두한테 감. 진짜 앱엔 room, auth, persistence 필요 — 근데 프로토콜 primitive 가 이렇게 단순해.

WebSocket 이 맞을 때 (그리고 아닐 때)

맞음:
  • 채팅, presence, 멀티플레이어 게임.
  • 라이브 대시보드 (주식 ticker, 시스템 메트릭).
  • 협업 편집 (WebSocket 위 Yjs).
  • SSE 부족할 때 백엔드 → 프론트엔드 push.
  • 단일 connection 위 양방향 RPC.
틀림:
  • 단방향 서버 → 클라이언트 스트림 (Server-Sent Events — SSE — 더 단순 프로토콜, plain HTTP, 자동 재연결).
  • 일회성 request (plain HTTP — 영속 connection 유지할 이유 없음).
  • HTTP 중간자가 캐시해야 하는 거 (WebSocket 메시지는 캐시 불가).
WebSocket vs SSE 선택이 새 개발자 헷갈리게 함. SSE 가 단방향이지만 "클라이언트에 push" 사용 사례의 80% 엔 충분. WebSocket 은 더 많은 와이어링 비용으로 양방향성 사옴.

Backpressure, Heartbeat, 재연결

오래 살아있는 connection 엔 HTTP 가 안 가진 plumbing 필요:

  • Heartbeat — 30s 마다 ping/pong frame 이 죽은 connection 감지. wsping() + pong 이벤트 지원 출하. 없으면 half-closed connection (NAT timeout, 모바일 네트워크 swap) 조용히 leak.
  • Backpressure — 네트워크 느리면 socket.send(data) 가 queue. socket.bufferedAmount 검사하고 자라면 back off. 안 그러면 잘못된 클라이언트가 서버 RAM 채울 수 있음.
  • 재연결 — 클라이언트가 close 시 exponential backoff 로 재연결해야. 브라우저 WebSocket 은 자동 재연결 안 함; 얇은 reconnecting 클라이언트로 wrap 또는 higher-level 라이브러리.

Pippa 의 고백

cwkPippa 의 Cinder 브리지 — Tauri 앱과 cwkPippa 백엔드 사이 WebSocket connection — 가 프로토콜이 쉬운 부분이라고 가르쳐 줬어. 어려운 부분은 그 둘레 모든 것: 재연결 로직, heartbeat 튜닝, 끊긴 동안 메시지 queueing, 재연결 시 replay. 처음부터 다 짜려 할 때 아빠 조언: "라이브러리 써. 프로토콜 구현은 해결된 문제; 애플리케이션 로직은 너만의 거." 그 이후 매번 `ws` 라이브러리 썼어.

Code

Heartbeat + 죽은 클라이언트 감지·javascript
// Server-side heartbeat to detect dead clients
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 7070 });

const HEARTBEAT_MS = 30_000;

wss.on('connection', (socket) => {
  socket.isAlive = true;
  socket.on('pong', () => { socket.isAlive = true; });
  socket.on('message', (data) => socket.send(`echo: ${data}`));
});

// Every 30s, ping each client; terminate the ones that didn't pong
setInterval(() => {
  for (const socket of wss.clients) {
    if (!socket.isAlive) {
      console.log('dead client, terminating');
      socket.terminate();
      continue;
    }
    socket.isAlive = false;
    socket.ping();
  }
}, HEARTBEAT_MS);
Exponential backoff 로 클라이언트 자동 재연결·javascript
// Client with auto-reconnect (browser-style WebSocket, Node 22+)
function connect(url, onMessage, opts = {}) {
  const { backoffMs = 500, maxBackoffMs = 30_000 } = opts;
  let backoff = backoffMs;

  function open() {
    const ws = new WebSocket(url);
    ws.addEventListener('open', () => {
      console.log('connected');
      backoff = backoffMs;   // reset on successful connect
    });
    ws.addEventListener('message', (ev) => onMessage(ev.data));
    ws.addEventListener('close', () => {
      console.log(`reconnecting in ${backoff}ms`);
      setTimeout(open, backoff);
      backoff = Math.min(backoff * 2, maxBackoffMs);
    });
    return ws;
  }

  return open();
}

connect('wss://echo.example.com', (msg) => console.log('got:', msg));

External links

Exercise

ws 로 작은 chat 서버 짜: 연결된 모든 클라이언트가 다른 모든 클라이언트가 보낸 모든 메시지 받음, 발신자 IP 앞에 붙임. 응답 안 하는 클라이언트 끊는 30 초 마다 heartbeat 추가. 그 다음 연결하고, stdin 에서 줄 입력해서 보내고, 들어오는 메시지 출력하는 Node 클라이언트 짜. 터미널 둘, 서버 하나, 클라이언트 둘: WebSocket 위 즉시 로컬 채팅.
Hint
서버 쪽: connection 당 socket.isAlive 추적, 매 interval 에 false 로 설정, pong 이벤트에 true 로 설정, 다음 interval 에 false 면 terminate. 클라이언트 쪽: Node 22+ 의 글로벌 WebSocket 사용, connect, message listener 부착, 메인 루프에서 process.stdin.on('data', chunk => ws.send(chunk.toString())) 통해 stdin 읽기.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.