"실시간"의 의미부터 정리
"실시간"이라는 단어 하나에 너무 많은 게 들어 있습니다. 화상 통화의 60ms와 댓글 알림의 5초는 둘 다 실시간이지만 요구가 정반대입니다. 잘못 고르면 과도하게 복잡한 인프라를 떠안거나, 반대로 사용자가 새로고침하게 만듭니다. 처음 던질 질문은 단 두 개입니다.
- 양방향인가, 단방향(서버→클라)인가?
- 메시지를 잃어도 되는가, 절대 잃으면 안 되는가?
이 두 질문의 답이 프로토콜 선택을 거의 결정합니다.
한 표로 보는 비교
WebSocket — 양방향이 필요할 때의 기본값
채팅·게임·협업 편집·라이브 입찰처럼 클라이언트가 실시간으로 보내고 받아야 하면 WebSocket입니다. 한 번 핸드셰이크가 끝나면 양방향 풀듀플렉스 채널이 열리고, 프레임 헤더가 6바이트로 작아 효율도 좋습니다.
다만 WebSocket은 HTTP 표준에서 한 발 비켜난 프로토콜이라 부수적인 일거리가 따라옵니다.
- 자동 재연결 — 직접 짜거나 Socket.IO·socket-cluster 같은 라이브러리에 의존
- 일부 기업 프록시·방화벽이 WS를 차단 — Socket.IO가 SSE/Polling 폴백을 자동으로 깔아주는 이유
- Sticky session — 다중 인스턴스 환경이라면 같은 클라이언트가 같은 서버에 붙도록 LB 설정
const ws = new WebSocket("wss://api.example.com/chat");
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// ...
};
ws.onclose = () => setTimeout(reconnect, 2000); // exponential backoff 권장
Server-Sent Events — 가장 저평가된 도구
알림·진행률·라이브 카운터·뉴스 피드처럼 서버 → 클라이언트 단방향이면 SSE가 압도적으로 우수합니다. 그런데 의외로 잘 안 쓰입니다.
- HTTP/HTTPS 그대로 — 모든 프록시·CDN·LB가 즉시 동작
- 자동 재연결이 표준 스펙에 내장 —
EventSource가 끊기면 알아서 재연결 Last-Event-ID헤더로 끊긴 동안의 메시지 재전송 가능
const es = new EventSource("/api/notifications");
es.onmessage = (e) => {
const msg = JSON.parse(e.data);
// ...
};
서버 쪽도 단순합니다.
// Next.js Route Handler 예시
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const enc = new TextEncoder();
while (true) {
const evt = await waitForEvent();
controller.enqueue(enc.encode(`id: ${evt.id}\ndata: ${JSON.stringify(evt)}\n\n`));
}
},
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
}
LLM 스트리밍 응답이 SSE를 다시 주류로 끌어올렸습니다. ChatGPT·Claude 응답이 한 글자씩 흘러나오는 것도 결국 SSE입니다.
Long Polling — 정말 환경이 험할 때
기업 내부망의 깐깐한 프록시·구형 클라이언트·방화벽 때문에 WS도 SSE도 못 쓸 때의 폴백입니다. 클라이언트가 요청을 보내면 서버가 메시지가 생길 때까지 응답을 보류하고, 응답을 받으면 클라이언트가 즉시 다음 요청을 보내는 구조입니다.
지연 시간이 다른 두 방식보다 약간 길고, 매 요청마다 헤더 오버헤드가 있어 효율은 떨어집니다. 신규로 선택할 일은 거의 없지만, 호환성 폴백으로는 여전히 가치가 있습니다.
MQTT — IoT라면 결국 여기로
WebSocket은 메시지 보장이 없습니다. SSE는 양방향이 안 됩니다. IoT 디바이스가 100대씩 붙고 정확한 메시지 도달이 필요하면 MQTT가 답입니다. 브로커 운영(Mosquitto·EMQX)이 추가 비용이긴 하지만, 디바이스 수가 늘어날수록 단위 비용은 떨어집니다. 토픽 기반 라우팅과 QoS 0/1/2의 보장 단계는 다른 프로토콜에선 직접 구현해야 하는 기능입니다.
선택 결정표
양방향이고, 메시지 손실 OK → WebSocket
양방향이고, 손실 절대 안 됨 → MQTT (브로커) 또는 WS + 메시지 ID·재전송 직접 구현
단방향(서버→클라), 단순 → SSE
단방향, 손실 절대 안 됨 → SSE + Last-Event-ID
양방향, 환경이 험함 → Long Polling 폴백
IoT 디바이스 100+ → MQTT
다음 단계
실시간을 다루기 시작하면 자연스럽게 메시지 큐(Redis Pub/Sub·NATS)가 들어옵니다. 한 서버가 받은 이벤트를 클러스터의 다른 서버에서도 구독자에게 전달해야 하기 때문입니다. 그 다음은 이벤트 소싱과 CDC(Change Data Capture) 까지 시야가 넓어지는데, 거기까지 가면 실시간 시스템을 처음부터 다시 설계할 준비가 된 셈입니다.