DevToolBox무료
블로그

WebSocket 튜토리얼: 실시간 통신 가이드

15분 읽기by DevToolBox

What Are WebSockets?

WebSockets provide a persistent, full-duplex communication channel between a client (browser) and a server over a single TCP connection. Unlike HTTP, where the client must initiate every request, WebSockets allow both the server and client to send data at any time. This makes WebSockets ideal for real-time applications like chat, live dashboards, collaborative editing, multiplayer games, and stock tickers.

This tutorial covers the WebSocket protocol, client-side and server-side implementation in JavaScript, error handling, reconnection strategies, and production best practices.

WebSocket vs HTTP: Key Differences

FeatureHTTPWebSocket
ConnectionNew connection per requestPersistent connection
DirectionClient-to-server (request/response)Bidirectional (full-duplex)
OverheadHeaders sent with every requestMinimal framing after handshake
LatencyHigher (connection setup each time)Lower (connection stays open)
Protocolhttp:// or https://ws:// or wss://
Use CaseREST APIs, page loadsReal-time data, live updates
Server PushNot native (use SSE or polling)Native server push

How WebSocket Handshake Works

A WebSocket connection starts as an HTTP request that gets upgraded to the WebSocket protocol. The client sends an upgrade request, and the server responds with a 101 Switching Protocols status.

# Client Request (HTTP Upgrade)
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

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

# After this handshake, the connection is upgraded
# and both sides can send WebSocket frames

Client-Side WebSocket API

The browser provides a native WebSocket API that is straightforward to use. Here is a complete client implementation with all event handlers.

// Basic WebSocket connection
const ws = new WebSocket('wss://echo.websocket.org');

// Connection opened
ws.addEventListener('open', (event) => {
  console.log('Connected to WebSocket server');
  ws.send('Hello Server!');
});

// Listen for messages
ws.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);

  // Handle different data types
  if (event.data instanceof Blob) {
    // Binary data
    const reader = new FileReader();
    reader.onload = () => console.log('Binary:', reader.result);
    reader.readAsArrayBuffer(event.data);
  } else {
    // Text data (usually JSON)
    try {
      const data = JSON.parse(event.data);
      console.log('Parsed:', data);
    } catch {
      console.log('Text:', event.data);
    }
  }
});

// Connection closed
ws.addEventListener('close', (event) => {
  console.log('Disconnected:', event.code, event.reason);
  console.log('Clean close:', event.wasClean);
});

// Error occurred
ws.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

// Send different data types
ws.send('Plain text message');
ws.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
ws.send(new Blob(['binary data']));
ws.send(new ArrayBuffer(8));

// Check connection state
console.log(ws.readyState);
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSING
// 3 = CLOSED

// Close connection gracefully
ws.close(1000, 'Normal closure');

Server-Side Implementation with Node.js

The most popular Node.js WebSocket library is ws. Here is a complete server implementation.

// server.ts - WebSocket server with Node.js
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

// Create HTTP server
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket server is running');
});

// Create WebSocket server
const wss = new WebSocketServer({ server });

// Track connected clients
const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, req) => {
  const clientId = generateId();
  clients.set(clientId, ws);
  console.log(`Client ${clientId} connected from ${req.socket.remoteAddress}`);

  // Send welcome message
  ws.send(JSON.stringify({
    type: 'welcome',
    clientId,
    message: 'Connected to WebSocket server',
  }));

  // Handle incoming messages
  ws.on('message', (data, isBinary) => {
    try {
      const message = JSON.parse(data.toString());
      console.log(`Received from ${clientId}:`, message);

      switch (message.type) {
        case 'chat':
          // Broadcast to all other clients
          broadcast({
            type: 'chat',
            from: clientId,
            text: message.text,
            timestamp: Date.now(),
          }, ws);
          break;

        case 'ping':
          ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
          break;

        default:
          ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' }));
      }
    } catch (err) {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
    }
  });

  // Handle client disconnect
  ws.on('close', (code, reason) => {
    clients.delete(clientId);
    console.log(`Client ${clientId} disconnected: ${code} ${reason}`);
    broadcast({
      type: 'system',
      message: `User ${clientId} left the chat`,
    });
  });

  // Handle errors
  ws.on('error', (error) => {
    console.error(`Client ${clientId} error:`, error);
    clients.delete(clientId);
  });

  // Heartbeat: detect dead connections
  (ws as any).isAlive = true;
  ws.on('pong', () => { (ws as any).isAlive = true; });
});

// Broadcast message to all clients except sender
function broadcast(data: object, exclude?: WebSocket) {
  const message = JSON.stringify(data);
  wss.clients.forEach(client => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Heartbeat interval to detect dead connections
const heartbeat = setInterval(() => {
  wss.clients.forEach(ws => {
    if ((ws as any).isAlive === false) {
      return ws.terminate();
    }
    (ws as any).isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => clearInterval(heartbeat));

// Start server
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

function generateId(): string {
  return Math.random().toString(36).substring(2, 9);
}

Robust Client with Auto-Reconnection

// WebSocket client with reconnection and message queue
class ReconnectingWebSocket {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 1000;
  private maxDelay = 30000;
  private messageQueue: string[] = [];
  private listeners = new Map<string, Set<Function>>();

  constructor(url: string) {
    this.url = url;
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;

      // Flush queued messages
      while (this.messageQueue.length > 0) {
        const msg = this.messageQueue.shift()!;
        this.ws!.send(msg);
      }

      this.emit('connected', {});
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.emit(data.type, data);
        this.emit('message', data);
      } catch {
        this.emit('message', { raw: event.data });
      }
    };

    this.ws.onclose = (event) => {
      console.log(`WebSocket closed: ${event.code}`);
      this.emit('disconnected', { code: event.code, reason: event.reason });

      if (event.code !== 1000) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = () => {
      this.emit('error', {});
    };
  }

  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.log('Max reconnection attempts reached');
      this.emit('maxRetriesReached', {});
      return;
    }

    const delay = Math.min(
      this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
      this.maxDelay
    );

    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);

    setTimeout(() => {
      this.reconnectAttempts++;
      this.connect();
    }, delay);
  }

  send(type: string, payload: any = {}) {
    const message = JSON.stringify({ type, ...payload });

    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      // Queue message for when connection is restored
      this.messageQueue.push(message);
    }
  }

  on(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
  }

  off(event: string, callback: Function) {
    this.listeners.get(event)?.delete(callback);
  }

  private emit(event: string, data: any) {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }

  close() {
    this.maxReconnectAttempts = 0; // Prevent reconnection
    this.ws?.close(1000, 'Client closing');
  }

  get state(): number {
    return this.ws?.readyState ?? WebSocket.CLOSED;
  }
}

// Usage
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');

ws.on('connected', () => console.log('Online'));
ws.on('disconnected', () => console.log('Offline'));
ws.on('chat', (data: any) => console.log('Chat:', data.text));

ws.send('chat', { text: 'Hello, World!' });

React Hook for WebSocket

// useWebSocket.ts - Custom React Hook
import { useEffect, useRef, useState, useCallback } from 'react';

type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';

interface UseWebSocketOptions {
  onMessage?: (data: any) => void;
  onOpen?: () => void;
  onClose?: (event: CloseEvent) => void;
  onError?: (event: Event) => void;
  reconnect?: boolean;
  reconnectInterval?: number;
  maxRetries?: number;
}

export function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
  const {
    onMessage,
    onOpen,
    onClose,
    onError,
    reconnect = true,
    reconnectInterval = 3000,
    maxRetries = 5,
  } = options;

  const [status, setStatus] = useState<WebSocketStatus>('connecting');
  const [lastMessage, setLastMessage] = useState<any>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const retriesRef = useRef(0);

  const connect = useCallback(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
    setStatus('connecting');

    ws.onopen = () => {
      setStatus('connected');
      retriesRef.current = 0;
      onOpen?.();
    };

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        setLastMessage(data);
        onMessage?.(data);
      } catch {
        setLastMessage(event.data);
        onMessage?.(event.data);
      }
    };

    ws.onclose = (event) => {
      setStatus('disconnected');
      onClose?.(event);

      if (reconnect && event.code !== 1000 && retriesRef.current < maxRetries) {
        retriesRef.current++;
        setTimeout(connect, reconnectInterval);
      }
    };

    ws.onerror = (event) => {
      setStatus('error');
      onError?.(event);
    };
  }, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval, maxRetries]);

  useEffect(() => {
    connect();
    return () => {
      wsRef.current?.close(1000);
    };
  }, [connect]);

  const send = useCallback((data: any) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(
        typeof data === 'string' ? data : JSON.stringify(data)
      );
    }
  }, []);

  return { status, lastMessage, send };
}

// Usage in a React component
function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<any[]>([]);

  const { status, send } = useWebSocket(
    `wss://api.example.com/chat/${roomId}`,
    {
      onMessage: (data) => {
        if (data.type === 'chat') {
          setMessages(prev => [...prev, data]);
        }
      },
    }
  );

  const handleSend = (text: string) => {
    send({ type: 'chat', text });
  };

  return (
    <div>
      <div>Status: {status}</div>
      <div>
        {messages.map((msg, i) => (
          <p key={i}>{msg.text}</p>
        ))}
      </div>
      <input
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            handleSend(e.currentTarget.value);
            e.currentTarget.value = '';
          }
        }}
        placeholder="Type a message..."
        disabled={status !== 'connected'}
      />
    </div>
  );
}

WebSocket with Socket.IO

// Socket.IO provides additional features on top of WebSockets:
// - Automatic reconnection
// - Room/namespace support
// - Fallback to HTTP long polling
// - Built-in acknowledgments

// Server (socket.io)
import { Server } from 'socket.io';

const io = new Server(3001, {
  cors: { origin: 'http://localhost:3000' },
});

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  // Join a room
  socket.on('join-room', (room: string) => {
    socket.join(room);
    io.to(room).emit('system', `${socket.id} joined ${room}`);
  });

  // Handle chat message
  socket.on('chat', (data, callback) => {
    io.to(data.room).emit('chat', {
      from: socket.id,
      text: data.text,
      timestamp: Date.now(),
    });
    // Acknowledge receipt
    callback({ status: 'delivered' });
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

// Client (socket.io-client)
import { io } from 'socket.io-client';

const socket = io('http://localhost:3001');

socket.on('connect', () => {
  socket.emit('join-room', 'general');
});

socket.on('chat', (data) => {
  console.log(`${data.from}: ${data.text}`);
});

// Send with acknowledgment
socket.emit('chat', { room: 'general', text: 'Hello!' }, (response: any) => {
  console.log('Message status:', response.status);
});

WebSocket Security Best Practices

  • Always use WSS (WebSocket Secure): Use wss:// in production, never ws://
  • Authenticate on connection: Validate JWT tokens or session cookies during the handshake
  • Rate limit messages: Prevent clients from flooding the server with messages
  • Validate all messages: Never trust client-sent data -- validate types, lengths, and content
  • Set message size limits: Configure maximum payload size to prevent memory attacks
  • Implement heartbeats: Use ping/pong frames to detect dead connections
  • Handle CORS properly: Restrict allowed origins in production
  • Use rooms for authorization: Only allow users to join rooms they have access to

Authentication Example

// Server-side authentication during WebSocket handshake
import { WebSocketServer } from 'ws';
import jwt from 'jsonwebtoken';

const wss = new WebSocketServer({
  server,
  verifyClient: (info, callback) => {
    const token = new URL(info.req.url!, 'http://localhost')
      .searchParams.get('token');

    if (!token) {
      callback(false, 401, 'Unauthorized');
      return;
    }

    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!);
      (info.req as any).user = decoded;
      callback(true);
    } catch {
      callback(false, 403, 'Invalid token');
    }
  },
});

// Client-side: pass token in URL
const token = localStorage.getItem('authToken');
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`);

WebSocket vs Server-Sent Events vs Long Polling

FeatureWebSocketSSELong Polling
DirectionBidirectionalServer to clientSimulated bidirectional
Protocolws:// / wss://HTTPHTTP
ConnectionPersistentPersistentRepeated requests
Browser SupportAll modernAll modernAll browsers
Binary DataYesNo (text only)Yes
Auto ReconnectManualBuilt-inManual
Proxy FriendlySometimes issuesYesYes
Best ForChat, gamingNotifications, feedsLegacy compatibility

Production Deployment Tips

  • Use a load balancer with sticky sessions: WebSocket connections must stay with the same server
  • Implement horizontal scaling: Use Redis pub/sub or similar to broadcast across multiple server instances
  • Monitor connection counts: Track active connections and set alerts for unusual spikes
  • Set connection limits per IP: Prevent a single client from opening too many connections
  • Configure Nginx for WebSocket: Add proper upgrade headers in your reverse proxy configuration
  • Graceful shutdown: Close all connections with proper close codes before server restart

Nginx WebSocket Proxy Configuration

# Nginx configuration for WebSocket proxy
upstream websocket_backend {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Timeout settings
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Conclusion

WebSockets are essential for any application requiring real-time, bidirectional communication. Start with the native browser WebSocket API for simple use cases, and consider Socket.IO or a custom reconnecting wrapper for production applications. Always implement proper error handling, reconnection logic, authentication, and message validation.

For simpler server-to-client streaming, consider Server-Sent Events (SSE) instead. For infrequent updates, regular HTTP polling may be sufficient. Choose the technology that matches your real-time requirements.

Test your WebSocket payloads with our JSON Formatter and debug JWT tokens in your authentication flow with the JWT Decoder.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

JWTJWT Decoder{ }JSON Formatter🔗URL ParserB64Base64 Encoder/Decoder

Related Articles

REST API 모범 사례: 2026년 완전 가이드

REST API 설계 모범 사례: 네이밍 규칙, 에러 처리, 인증, 페이지네이션, 보안을 배웁니다.

API 인증: OAuth 2.0 vs JWT vs API Key

API 인증 방법 비교: OAuth 2.0, JWT Bearer 토큰, API Key. 각 방법의 사용 시나리오, 보안 절충안, 구현 패턴.

CORS 에러 해결 완벽 가이드

CORS 에러를 단계별로 해결하세요. Access-Control-Allow-Origin, preflight, credentials, 서버 설정을 다룹니다.