WebSocket 완벽 가이드 | 실시간 통신·Socket.io·채팅·알림·게임
이 글의 핵심
WebSocket으로 실시간 통신을 구현하는 완벽 가이드입니다. WebSocket API, Socket.io, 채팅, 실시간 알림, 멀티플레이 게임까지 실전 예제로 정리했습니다.
실무 경험 공유: 폴링 방식의 채팅을 WebSocket으로 전환하면서, 서버 부하를 90% 줄이고 메시지 지연을 3초에서 즉시로 개선한 경험을 공유합니다.
들어가며: “실시간 업데이트가 필요해요”
실무 문제 시나리오
시나리오 1: 폴링이 비효율적이에요
1초마다 API를 호출합니다. 서버 부하가 큽니다. WebSocket은 연결 유지로 효율적입니다.
시나리오 2: 메시지가 지연돼요
폴링 간격만큼 지연됩니다. WebSocket은 즉시 전달됩니다.
시나리오 3: 실시간 협업이 필요해요
Google Docs 같은 기능이 필요합니다. WebSocket으로 구현합니다.
1. WebSocket이란?
핵심 특징
WebSocket은 양방향 실시간 통신 프로토콜입니다.
주요 장점:
- 양방향: 서버 ↔ 클라이언트
- 실시간: 즉시 전달
- 효율적: 연결 유지
- 낮은 지연: < 10ms
- 표준: 브라우저 내장
HTTP vs WebSocket:
- HTTP: 요청/응답 모델
- WebSocket: 지속적 연결
2. 기본 WebSocket API
서버 (Node.js)
// server.ts
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (data) => {
console.log('Received:', data.toString());
// 에코
ws.send(`Echo: ${data}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
ws.send('Welcome!');
});
console.log('WebSocket server running on ws://localhost:8080');
클라이언트 (브라우저)
// client.ts
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('Connected');
ws.send('Hello Server!');
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (error) => {
console.error('Error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
};
3. Socket.io
설치
# 서버
npm install socket.io
# 클라이언트
npm install socket.io-client
서버
// server.ts
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('message', (data) => {
console.log('Message:', data);
// 모든 클라이언트에게 전송
io.emit('message', data);
// 발신자 제외
socket.broadcast.emit('message', data);
// 특정 클라이언트에게
socket.to(targetSocketId).emit('message', data);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
httpServer.listen(3000, () => {
console.log('Server running on :3000');
});
클라이언트
// client.ts
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('message', (data) => {
console.log('Received:', data);
});
socket.emit('message', { text: 'Hello!' });
4. 실전 예제: 채팅 앱
서버
// server.ts
import { Server } from 'socket.io';
const io = new Server(3000, {
cors: { origin: '*' },
});
interface Message {
user: string;
text: string;
timestamp: string;
}
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// 방 입장
socket.on('join', (room: string) => {
socket.join(room);
socket.to(room).emit('user-joined', socket.id);
console.log(`${socket.id} joined ${room}`);
});
// 메시지 전송
socket.on('message', (data: { room: string; message: Message }) => {
io.to(data.room).emit('message', data.message);
});
// 타이핑 중
socket.on('typing', (room: string) => {
socket.to(room).emit('typing', socket.id);
});
// 연결 해제
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
클라이언트 (React)
// Chat.tsx
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
let socket: Socket;
export function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const room = 'general';
useEffect(() => {
socket = io('http://localhost:3000');
socket.emit('join', room);
socket.on('message', (message: Message) => {
setMessages((prev) => [...prev, message]);
});
socket.on('typing', (userId: string) => {
console.log(`${userId} is typing...`);
});
return () => {
socket.disconnect();
};
}, []);
const sendMessage = () => {
if (input.trim()) {
const message: Message = {
user: 'Me',
text: input,
timestamp: new Date().toISOString(),
};
socket.emit('message', { room, message });
setInput('');
}
};
const handleTyping = () => {
socket.emit('typing', room);
};
return (
<div>
<div>
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.user}:</strong> {msg.text}
</div>
))}
</div>
<input
value={input}
onChange={(e) => {
setInput(e.target.value);
handleTyping();
}}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
5. Redis 통합 (확장성)
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const io = new Server(3000);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter connected');
});
// 이제 여러 서버 인스턴스가 메시지를 공유합니다
정리 및 체크리스트
핵심 요약
- WebSocket: 양방향 실시간 통신
- Socket.io: 편리한 추상화
- Room: 그룹 메시징
- Broadcasting: 다중 전송
- Redis 통합: 수평 확장
- 낮은 지연: < 10ms
구현 체크리스트
- WebSocket 서버 구현
- Socket.io 통합
- Room 기능 구현
- 인증 구현
- 에러 처리
- Redis 통합 (확장성)
- 배포
같이 보면 좋은 글
- Redis 고급 가이드
- NestJS 완벽 가이드
- Kubernetes 실전 가이드
이 글에서 다루는 키워드
WebSocket, Real-time, Socket.io, Chat, Node.js, Backend
자주 묻는 질문 (FAQ)
Q. WebSocket vs Server-Sent Events, 어떤 게 나은가요?
A. WebSocket은 양방향입니다. SSE는 서버 → 클라이언트만 가능합니다. 양방향이 필요하면 WebSocket을 사용하세요.
Q. WebSocket vs HTTP/2, 차이가 뭔가요?
A. HTTP/2는 요청/응답 모델입니다. WebSocket은 지속적 연결입니다. 실시간 통신은 WebSocket을 사용하세요.
Q. 연결이 끊기면 어떻게 되나요?
A. Socket.io는 자동으로 재연결을 시도합니다. 네이티브 WebSocket은 수동으로 재연결해야 합니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, Slack, Trello, WhatsApp Web 등에서 사용합니다.