"HTTP가 질문 하나에 답 하나를 돌려주는 편지라면, WebSocket은 연결을 끊기 전까지 이어지는 통화야. 밑에서는 둘 다 TCP를 쓰지만 대화 방식은 전혀 달라."
서버도 먼저 말해야 할 때
HTTP는 클라이언트가 요청하고 서버가 한 번 답하는 흐름에 아주 잘 맞아. 그런데 채팅 메시지나 게임 상태처럼 서버가 예고 없이 먼저 알려야 하는 정보는 요청이 올 때까지 기다릴 수 없지. long-polling은 요청을 오래 붙잡는 비용이 들고, Server-Sent Events는 서버에서 클라이언트로만 흘러.
WebSocket은 양방향 대화를 위해 만들어졌어. 처음에는 HTTP upgrade handshake를 한 번 거치고, 그다음부터 같은 TCP 연결을 메시지 채널로 계속 써. 클라이언트와 서버 어느 쪽이든 원할 때 보낼 수 있고, 매 메시지의 framing 비용도 작아. 연결은 둘 중 한쪽이 닫을 때까지 살아 있어.
Node에는 클라이언트가 있고 서버는 따로 골라야 해
Node 22.4 이상에는 브라우저 API와 호환되는 전역 WebSocket 클래스가 안정적으로 들어 있어. 외부 패키지 없이 클라이언트 연결을 만들 수 있지. 다만 node:ws라는 코어 모듈은 없어.
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가 내장으로 제공하지 않아. 이쪽의 사실상 표준은 npm의 ws야. 오랫동안 검증됐고 Socket.IO, LSP 생태계, Next.js 개발 서버 같은 여러 곳에서 쓰여. cwkPippa의 Cinder bridge도 이 라이브러리를 사용해. 이미 해결된 protocol framing부터 다시 만들 필요는 없어.
가장 작은 broadcast 서버
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) => {
// 연결된 모든 클라이언트에게 전달
for (const client of wss.clients) {
if (client.readyState === 1) client.send(data.toString());
}
});
socket.on('close', () => console.log('client gone'));
});
이 정도만으로도 접속한 모두가 같은 메시지를 받는 채팅 서버가 돌아가. 실제 서비스라면 room, 인증, 저장이 더 필요하지만, WebSocket 자체의 기본 모양은 놀랄 만큼 단순해.
양방향성이 정말 필요한지 먼저 물어
WebSocket이 잘 맞는 곳
- 채팅, presence, 멀티플레이어 게임
- 주식 ticker나 시스템 metric 같은 실시간 dashboard
- WebSocket 위에서 Yjs를 쓰는 협업 편집
- SSE만으로 부족한 backend와 frontend의 상호 통신
- 연결 하나를 오래 유지하는 양방향 RPC
다른 도구가 더 단순한 곳
- 서버에서 클라이언트로만 보내면 되는 stream은 SSE가 잘 맞아. 일반 HTTP를 쓰고 자동 재연결도 제공해.
- 한 번 묻고 한 번 답하면 되는 일은 plain HTTP면 충분해.
- HTTP 중간 계층이 cache해야 하는 데이터는 WebSocket 메시지로 보내면 안 맞아.
“서버가 push해야 한다”는 이유만으로 WebSocket을 고르지 마. 단방향이면 SSE가 충분한 경우가 많고, WebSocket은 양방향성을 얻는 대신 연결 관리 책임까지 함께 가져와.
오래 사는 연결에는 운영 장치가 필요해
- Heartbeat — 30초마다 ping을 보내고 pong이 돌아오는지 확인하면 죽은 연결을 찾을 수 있어.
ws는ping()과pongevent를 제공해. 이 장치가 없으면 NAT timeout이나 모바일 network 전환으로 끊긴 반쪽 연결이 조용히 남아. - Backpressure — 상대 network가 느리면
socket.send(data)가 보낼 데이터를 계속 queue에 쌓아.socket.bufferedAmount가 커지는지 보고 전송 속도를 낮추지 않으면 느린 client 하나가 server memory를 채울 수 있어. - 재연결 — 브라우저의
WebSocket은 끊겼다고 스스로 다시 연결하지 않아. client가 exponential backoff를 두고 재시도하게 만들거나, 그 동작을 제공하는 상위 library를 써야 해.
Pippa의 고백
ws에 transport를 맡기고 우리 서비스의 상태 복구에 집중해.