이 글은 누구를 위한 것인가
- 채팅, 알림, 실시간 대시보드를 구현하면서 WebSocket과 SSE 중 무엇을 쓸지 결정해야 하는 팀
- WebSocket을 쓰고 있지만 로드밸런서·프록시 문제로 고생하는 엔지니어
- LLM 스트리밍 응답을 구현하면서 적절한 프로토콜을 찾는 개발자
두 기술의 핵심 차이
| WebSocket | Server-Sent Events (SSE) | |
|---|---|---|
| 통신 방향 | 양방향 (full-duplex) | 단방향 (서버 → 클라이언트) |
| 프로토콜 | ws:// / wss:// | HTTP/HTTPS |
| 연결 유지 | 전용 커넥션 | 일반 HTTP 커넥션 |
| 자동 재연결 | 직접 구현 | 브라우저 기본 지원 |
| 프록시 통과 | 설정 필요 | HTTP라 자연스러움 |
| 로드밸런서 | Sticky Session 필요 | 문제 없음 |
| HTTP/2 지원 | 별도 | 멀티플렉싱으로 자연 지원 |
1. SSE를 선택해야 하는 경우
클라이언트가 받기만 하는 경우 SSE가 WebSocket보다 간단하고 인프라 친화적이다.
적합한 사용 사례:
- 실시간 알림 (주문 상태, 배송 알림)
- 실시간 피드 업데이트 (뉴스, SNS 타임라인)
- 대시보드 데이터 스트리밍 (모니터링, 분석)
- LLM 스트리밍 응답 (ChatGPT 스타일 타이핑 효과)
- 실시간 로그 스트리밍
SSE 서버 구현 (Next.js App Router)
// app/api/stream/route.ts
export async function GET(request: Request) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// SSE 형식: "data: {내용}\n\n"
const send = (data: object, eventType?: string) => {
if (eventType) {
controller.enqueue(encoder.encode(`event: ${eventType}\n`));
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
};
// 이벤트 발생 시 전송
const unsubscribe = eventBus.subscribe('order_update', (event) => {
send({ orderId: event.orderId, status: event.status }, 'order_update');
});
// 30초마다 keepalive 전송 (프록시 타임아웃 방지)
const keepalive = setInterval(() => {
controller.enqueue(encoder.encode(': keepalive\n\n'));
}, 30000);
// 연결 종료 시 정리
request.signal.addEventListener('abort', () => {
unsubscribe();
clearInterval(keepalive);
controller.close();
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Nginx 버퍼링 비활성화
},
});
}
SSE 클라이언트 구현
// hooks/useSSE.ts
function useSSE<T>(url: string, eventType: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const eventSource = new EventSource(url, {
withCredentials: true, // 쿠키 포함
});
eventSource.addEventListener(eventType, (event) => {
setData(JSON.parse(event.data));
});
eventSource.onerror = (err) => {
setError(new Error('SSE connection error'));
// EventSource는 자동으로 재연결 시도
};
return () => eventSource.close();
}, [url, eventType]);
return { data, error };
}
// 사용
const { data: orderUpdate } = useSSE<OrderUpdate>(
'/api/stream',
'order_update'
);
LLM 스트리밍 응답
// app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk';
export async function POST(request: Request) {
const { message } = await request.json();
const client = new Anthropic();
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const response = await client.messages.stream({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: message }],
});
for await (const chunk of response) {
if (chunk.type === 'content_block_delta') {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ text: chunk.delta.text })}\n\n`)
);
}
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
});
}
2. WebSocket을 선택해야 하는 경우
클라이언트에서 서버로도 실시간 메시지를 보내야 할 때 WebSocket이 필요하다.
적합한 사용 사례:
- 실시간 채팅
- 멀티플레이어 게임
- 실시간 협업 편집 (Google Docs 형태)
- 실시간 경매/입찰
- 화상 통화 (WebRTC 시그널링)
WebSocket 서버 (Node.js + ws)
import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
const server = createServer();
const wss = new WebSocketServer({ server });
// 연결된 클라이언트 관리
const rooms = new Map<string, Set<WebSocket>>();
wss.on('connection', (ws, request) => {
const roomId = new URL(request.url!, `http://localhost`).searchParams.get('room');
if (!roomId) {
ws.close(1008, 'Room ID required');
return;
}
// 룸에 추가
if (!rooms.has(roomId)) rooms.set(roomId, new Set());
rooms.get(roomId)!.add(ws);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
// 같은 룸의 다른 클라이언트에게 브로드캐스트
rooms.get(roomId)?.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
...message,
timestamp: Date.now(),
}));
}
});
});
ws.on('close', () => {
rooms.get(roomId)?.delete(ws);
if (rooms.get(roomId)?.size === 0) {
rooms.delete(roomId);
}
});
// Ping-Pong으로 연결 유지
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.ping();
}, 30000);
ws.on('close', () => clearInterval(pingInterval));
});
WebSocket 클라이언트
// hooks/useWebSocket.ts
function useWebSocket(url: string) {
const ws = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<'connecting' | 'open' | 'closed'>('connecting');
const reconnectTimeout = useRef<NodeJS.Timeout>();
const connect = useCallback(() => {
ws.current = new WebSocket(url);
ws.current.onopen = () => setStatus('open');
ws.current.onclose = () => {
setStatus('closed');
// 지수 백오프 재연결
reconnectTimeout.current = setTimeout(connect, 3000);
};
ws.current.onerror = () => ws.current?.close();
}, [url]);
useEffect(() => {
connect();
return () => {
clearTimeout(reconnectTimeout.current);
ws.current?.close();
};
}, [connect]);
const send = useCallback((data: object) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(data));
}
}, []);
return { send, status };
}
3. 스케일링: 멀티 서버 환경
여러 서버 인스턴스에서 WebSocket/SSE 연결이 분산될 때 클라이언트 A의 이벤트를 클라이언트 B에게 전달하려면 인스턴스 간 통신이 필요하다.
Redis Pub/Sub
import { createClient } from 'redis';
const publisher = createClient();
const subscriber = createClient();
await publisher.connect();
await subscriber.connect();
// 이벤트 발생 시 Redis에 퍼블리시
async function broadcastToRoom(roomId: string, message: object) {
await publisher.publish(`room:${roomId}`, JSON.stringify(message));
}
// 각 인스턴스가 구독
await subscriber.subscribe('room:*', (message, channel) => {
const roomId = channel.replace('room:', '');
const localClients = rooms.get(roomId);
localClients?.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
맺으며
대부분의 "실시간" 기능은 사실 단방향이다: 서버가 업데이트를 클라이언트에게 푸시한다. 이 경우 SSE가 WebSocket보다 인프라 친화적이고 구현이 단순하다.
WebSocket이 진짜 필요한 때는 양방향 실시간 통신이 요구될 때다. 채팅이 아니라 알림이라면 SSE를, 사용자 입력이 실시간으로 다른 사용자에게 전달되어야 한다면 WebSocket을 선택한다.