TechFeedTechFeed
Backend

WebSocket 실전 구현 가이드

HTTP vs WebSocket, Socket.IO, ws 라이브러리, 재연결 전략, 보안 설정 실전 가이드.

한 줄 요약: WebSocket은 HTTP 핸드셰이크로 연결을 시작한 뒤 양방향 지속 연결을 유지한다. 채팅, 실시간 대시보드, 멀티플레이어 게임처럼 서버에서 클라이언트로 데이터를 "밀어야" 하는 경우에 HTTP 폴링보다 훨씬 효율적이다.

실시간 기능을 구현하려 할 때 가장 먼저 드는 질문은 "WebSocket을 직접 쓸까, Socket.IO를 쓸까"다. 이 글은 HTTP와 WebSocket의 차이, Node.js에서 WebSocket 서버와 클라이언트를 구현하는 방법, Socket.IO의 기능과 트레이드오프, 연결 끊김 대응 재연결 전략, 그리고 보안 설정까지 실전에 필요한 내용을 다룬다.

HTTP vs WebSocket — 언제 무엇을 쓰나

HTTP는 요청-응답 모델이다. 클라이언트가 요청해야 서버가 응답한다. 서버가 먼저 데이터를 보낼 수 없다.

실시간 데이터를 HTTP로 구현하는 방법들이 있지만 각각 한계가 있다.

  • 폴링(Polling): 클라이언트가 주기적으로 GET /updates를 요청한다. 새 데이터가 없어도 요청한다. 서버 부하와 불필요한 네트워크 트래픽이 발생한다.
  • 롱 폴링(Long Polling): 서버가 새 데이터가 생길 때까지 응답을 지연한다. 개선이지만 연결이 반복적으로 끊기고 재연결된다.
  • SSE(Server-Sent Events): 서버가 클라이언트로 단방향 스트림을 보낼 수 있다. 단방향이라 클라이언트 → 서버 통신은 별도 HTTP 요청이 필요하다.

WebSocket은 HTTP Upgrade 헤더를 사용해 초기 핸드셰이크 후 TCP 연결을 유지한다. 이후 서버와 클라이언트 모두 언제든 메시지를 보낼 수 있다. 양방향, 저지연, 낮은 오버헤드가 특징이다.

방식방향지연서버 부하사용 사례
HTTP Polling단방향높음높음단순 상태 확인
SSE서버→클라이언트낮음중간알림, 피드
WebSocket양방향매우 낮음낮음채팅, 게임, 협업

ws 라이브러리로 WebSocket 서버 구현

ws는 Node.js의 표준 WebSocket 라이브러리다. Socket.IO보다 가볍고, WebSocket 프로토콜을 직접 다룬다.

ws 라이브러리 — 서버 구현 (Node.js)
// npm install ws const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 8080 }); // 연결된 클라이언트를 관리하는 Set const clients = new Set(); wss.on('connection', (ws, req) => { // 클라이언트 IP (프록시 뒤에서는 X-Forwarded-For 사용) const ip = req.socket.remoteAddress; console.log(`Client connected: ${ip}`); clients.add(ws); // 클라이언트로부터 메시지 수신 ws.on('message', (data) => { let message; try { message = JSON.parse(data.toString()); } catch { ws.send(JSON.stringify({ error: 'Invalid JSON' })); return; } console.log('Received:', message); // 브로드캐스트: 연결된 모든 클라이언트에 전송 const payload = JSON.stringify({ type: 'broadcast', data: message }); for (const client of clients) { if (client.readyState === WebSocket.OPEN) { client.send(payload); } } }); // 연결 종료 ws.on('close', () => { clients.delete(ws); console.log(`Client disconnected: ${ip}`); }); // 에러 처리 ws.on('error', (err) => { console.error('WebSocket error:', err); clients.delete(ws); }); // 연결 확인 메시지 전송 ws.send(JSON.stringify({ type: 'connected', message: 'Welcome' })); }); console.log('WebSocket server running on ws://localhost:8080');
ws 라이브러리 — 클라이언트 구현 (브라우저)
// 브라우저 내장 WebSocket API 사용 (별도 설치 불필요) const ws = new WebSocket('ws://localhost:8080'); ws.addEventListener('open', () => { console.log('Connected to server'); ws.send(JSON.stringify({ type: 'hello', text: '안녕하세요' })); }); ws.addEventListener('message', (event) => { const data = JSON.parse(event.data); console.log('Received:', data); }); ws.addEventListener('close', (event) => { console.log(`Connection closed: code=${event.code}, reason=${event.reason}`); // 재연결 로직 실행 }); ws.addEventListener('error', (error) => { console.error('WebSocket error:', error); }); // 메시지 전송 function sendMessage(text) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'message', text })); } }

Socket.IO — WebSocket 위의 추상 레이어

Socket.IO는 WebSocket 위에 추가 기능을 얹은 라이브러리다. WebSocket을 직접 쓰는 것보다 편리하지만, 클라이언트와 서버 모두 Socket.IO를 써야 한다는 의존성이 생긴다.

Socket.IO가 ws 대비 추가 제공하는 기능

  • 자동 재연결: 연결이 끊기면 지수 백오프로 자동 재시도한다.
  • 룸(Room): 클라이언트를 그룹(룸)으로 묶어 그룹 내에서만 브로드캐스트 가능. 채팅 채널 구현에 편리하다.
  • 네임스페이스: 하나의 서버에서 논리적으로 분리된 연결 공간을 만들 수 있다.
  • 폴백: WebSocket이 불가한 환경(일부 기업 방화벽)에서 HTTP 롱 폴링으로 자동 폴백한다.
  • 이벤트 기반 API: socket.emit('message', data) / socket.on('message', handler)로 이벤트 이름을 지정할 수 있어 메시지 타입 관리가 편하다.

Socket.IO를 선택하지 않을 이유

  • Socket.IO 클라이언트 라이브러리 자체가 번들 크기를 늘린다.
  • 표준 WebSocket 클라이언트(브라우저, 모바일 앱)와 직접 연동되지 않는다.
  • Socket.IO 서버를 여러 인스턴스로 수평 확장하려면 Redis Adapter가 필요하다.

재연결 전략 — 연결 끊김 대응

WebSocket 연결은 네트워크 불안정, 서버 재시작, 클라이언트 절전 등으로 예고 없이 끊긴다. 재연결 로직은 필수다.

지수 백오프 재연결 구현 (브라우저)
class ReconnectingWebSocket { constructor(url) { this.url = url; this.reconnectDelay = 1000; // 첫 재연결 대기: 1초 this.maxDelay = 30000; // 최대 대기: 30초 this.reconnectAttempts = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.addEventListener('open', () => { console.log('Connected'); this.reconnectDelay = 1000; // 성공 시 딜레이 초기화 this.reconnectAttempts = 0; }); this.ws.addEventListener('message', (event) => { this.onMessage(JSON.parse(event.data)); }); this.ws.addEventListener('close', (event) => { // 1000: 정상 종료, 1001: 서버 종료 — 재연결 안 함 if (event.code === 1000 || event.code === 1001) return; this.scheduleReconnect(); }); this.ws.addEventListener('error', () => { this.ws.close(); }); } scheduleReconnect() { this.reconnectAttempts++; // 지수 백오프 + 지터(jitter)로 동시 재연결 분산 const jitter = Math.random() * 500; const delay = Math.min(this.reconnectDelay * 2 ** (this.reconnectAttempts - 1), this.maxDelay) + jitter; console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => this.connect(), delay); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } } onMessage(data) { // 서브클래스에서 오버라이드 console.log('Message:', data); } }

WebSocket 보안 설정

WebSocket 서버는 HTTP보다 보안 설정을 빠뜨리기 쉽다. 필수 설정 목록이다.

WSS (WebSocket Secure)

프로덕션에서는 반드시 wss:// (TLS over WebSocket)를 사용해야 한다. Nginx나 Cloudflare 앞에 두어 TLS 종료를 처리하면 애플리케이션 코드는 변경 없이 ws://를 유지할 수 있다.

Origin 검증

WebSocket 핸드셰이크 시 Origin 헤더를 확인해야 한다. 브라우저는 Origin 헤더를 조작할 수 없으므로 CSRF 방어가 가능하다.

인증

WebSocket은 쿠키를 핸드셰이크 시 자동으로 보내지만, 프로그래매틱 클라이언트(서버, 모바일 앱)에서는 쿠키가 없다. 연결 후 첫 메시지로 JWT 토큰을 보내거나, 핸드셰이크 URL에 토큰을 쿼리 파라미터로 포함하는 방법을 쓴다. URL에 토큰을 넣으면 서버 로그에 노출될 수 있으니 짧은 만료 시간의 1회용 토큰을 사용하는 것이 권장된다.

Rate Limiting과 메시지 크기 제한

연결 수, 메시지 전송 속도, 메시지 크기 모두 제한해야 한다. 제한이 없으면 단일 클라이언트가 서버를 압도할 수 있다.

실무 팁: 소규모 프로젝트에서 실시간 기능이 필요하다면 WebSocket 서버를 직접 구현하는 대신 Supabase Realtime, Ably, Pusher 같은 관리형 서비스를 먼저 검토하라. 연결 관리, 스케일링, 재연결 처리를 대신해주며, 초기 개발 속도를 크게 높인다.
주의 — 수평 확장: WebSocket은 상태가 있는(stateful) 연결이다. 여러 서버 인스턴스를 운영하면 클라이언트 A가 서버 1에, 클라이언트 B가 서버 2에 연결된 경우 브로드캐스트가 같은 서버에 연결된 클라이언트에만 전달된다. Redis Pub/Sub이나 메시지 큐를 서버 간 브로드캐스트 채널로 사용하면 이 문제를 해결할 수 있다.
WebSocket실시간Socket.IO백엔드통신

관련 포스트

REST API 설계 모범 사례 20262026-02-22Redis 실전 활용 패턴 7가지2026-02-24GraphQL vs REST API — 2026년 비교2026-03-10마이크로서비스 vs 모놀리스 — 현실적 선택 기준2026-03-11