Server-Sent Events (SSE): 간단하지만 강력한 실시간 통신
SSE란?
Server-Sent Events(SSE)는 HTTP를 통해 서버가 클라이언트에게 실시간으로 데이터를 푸시하는 표준 기술이다.
const eventSource = new EventSource("/api/stream");
// 연결 성공 시
eventSource.onopen = () => {
console.log("✅ 연결 성공");
};
// 메시지 수신 시
eventSource.onmessage = (event) => {
console.log("📨 새 데이터:", event.data);
};
// 에러 발생 시 (자동 재연결 시도)
eventSource.onerror = (error) => {
console.error("❌ 연결 오류:", error);
};
// 연결 종료 (수동)
eventSource.close();
주요 특징:
- HTTP 기반의 단방향 통신 (서버 → 클라이언트)
- 자동 재연결 지원
- 기존 HTTP 인프라 활용
적합한 사용 사례:
- 실시간 알림, 뉴스 피드
- 주식 시세, 대시보드 업데이트
- 진행 상태 모니터링
- LLM 스트리밍 응답 (ChatGPT, Claude 등)
작동 원리
SSE는 HTTP/1.1의 Chunked Transfer Encoding을 활용한다. 서버는 응답을 종료하지 않고 계속해서 데이터를 전송한다:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
data: 첫 번째 메시지\n\n
data: 두 번째 메시지\n\n
data: 세 번째 메시지\n\n
... (계속)
일반 HTTP는 0\r\n\r\n (종료 청크)를 보내며 끝나지만, SSE는 절대 종료하지 않는다. 이렇게 연결을 열어둔 채로 새 이벤트를 계속 전송한다.
이벤트 형식
event: userUpdate
data: {"userId": 123, "status": "online"}
id: 1
event: 이벤트 타입 (미지정시 'message')data: 실제 데이터id: 이벤트 ID (재연결 시 필요)- 메시지는 빈 줄(
\n\n)로 구분
재연결 메커니즘
연결이 끊어지면 브라우저가 자동으로 재연결하며, Last-Event-ID 헤더로 마지막 이벤트 ID를 전송한다:
// 서버에서 확인
const lastEventId = req.headers["last-event-id"];
if (lastEventId) {
// 누락된 이벤트만 재전송
sendMissedEvents(lastEventId);
}
이를 통해 중간에 놓친 메시지를 자동으로 복구할 수 있다.
HTTP/2의 중요성
HTTP/1.1의 문제:
- 브라우저당 도메인당 6개 연결 제한
- SSE가 연결을 영구 점유 → 다른 요청 차단
HTTP/2의 해결책:
- 단일 연결에서 100+ 스트림 동시 처리
- Multiplexing으로 SSE와 일반 요청 공존
HTTP/1.1: 6개 연결 제한 → SSE 3개면 나머지 3개로 모든 요청 처리
HTTP/2: 하나의 연결 → SSE + API + 이미지 등 100+ 동시 처리
결론: 프로덕션에서는 HTTP/2 필수
WebSocket과의 차이
| 특징 | SSE | WebSocket |
|---|---|---|
| 통신 방향 | 단방향 | 양방향 |
| 재연결 | 자동 | 직접 구현 |
| 프로토콜 | HTTP | WebSocket |
| 복잡도 | 낮음 | 높음 |
언제 SSE를 사용할까?
✅ SSE가 적합한 경우:
- 실시간 알림, 뉴스 피드
- 대시보드, 모니터링
- LLM 스트리밍 응답 (ChatGPT, Claude 등)
- 진행 상태 추적
- 서버 → 클라이언트 단방향 데이터 푸시
언제 WebSocket을 사용할까?
❌ WebSocket이 필요한 경우:
- 양방향 통신 (채팅, 게임, 협업 도구)
- 바이너리 데이터 전송
- 매우 낮은 지연시간이 중요한 경우
- 클라이언트 → 서버 실시간 전송 필요
보안 체크리스트
- 인증: Cookie 기반 권장 (EventSource는 커스텀 헤더 미지원)
- CSRF: Origin 헤더 검증 + SameSite 쿠키
- XSS:
textContent사용 (innerHTML 금지) - HTTPS: 필수
직접 체험해보기
아래 데모에서 SSE 기반 AI 스트리밍을 실시간으로 체험할 수 있다. 질문을 입력하고 전송 버튼을 눌러보자.
참고: 이 데모는 실제 AI API를 호출하지 않으며, 미리 준비된 응답을 스트리밍한다. 실제 구현에서는 OpenAI, Anthropic 등의 API를 연동하면 된다.
실전 구현: AI 스트리밍 데모 분석
위 데모의 실제 구현을 단계별로 살펴보자.
1. 서버: Next.js API Route
/api/ai-stream에서 ReadableStream으로 문자 단위 스트리밍을 구현한다.
// app/api/ai-stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// ① 연결 확인 메시지
controller.enqueue(encoder.encode('data: {"status": "connected"}\n\n'));
// ② Mock 데이터 문자 단위 스트리밍 (20-40ms 지연)
for (const char of mockText) {
const delay = 20 + Math.random() * 20;
await new Promise((resolve) => setTimeout(resolve, delay));
const chunk = JSON.stringify({ text: char, done: false });
controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
}
// ③ 완료 신호
controller.enqueue(encoder.encode('data: {"done": true}\n\n'));
controller.close();
},
});
// SSE 필수 헤더
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
핵심 포인트:
ReadableStream으로 문자를 하나씩 전송text/event-stream헤더로 SSE 프로토콜 준수done: true로 스트리밍 종료 신호 전달
2. 클라이언트: useAiStream Hook
EventSource를 캡슐화하여 상태 관리를 제공한다.
// features/sse-demo/lib/use-ai-stream.ts
export function useAiStream() {
const [streamedText, setStreamedText] = useState("");
const [connectionState, setConnectionState] =
useState<ConnectionState>("idle");
const eventSourceRef = useRef<EventSource | null>(null);
const startStream = useCallback(() => {
const es = new EventSource("/api/ai-stream");
eventSourceRef.current = es;
es.onopen = () => {
setConnectionState("connected");
};
es.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === "connected") return; // 연결 확인 무시
if (data.done) {
// 완료 처리
es.close();
setConnectionState("disconnected");
return;
}
// 텍스트 누적
if (data.text) {
setStreamedText((prev) => prev + data.text);
}
};
es.onerror = () => {
setConnectionState("error");
es.close();
};
}, []);
// cleanup: 언마운트 시 연결 종료
useEffect(() => {
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
}, []);
return { streamedText, connectionState, startStream, stopStream };
}
핵심 포인트:
useRef로 EventSource 인스턴스 보관 (재렌더링 방지)- 연결 상태를 5단계로 추적 (
idle→connecting→connected→disconnected) - cleanup 함수로 메모리 누수 방지
3. UI: AiStreamDemo Component
채팅 인터페이스와 자동 스크롤을 구현한다.
// features/sse-demo/ui/ai-stream-demo.tsx
export function AiStreamDemo() {
const [messages, setMessages] = useState<Message[]>([]);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { streamedText, connectionState, startStream } = useAiStream();
// 자동 스크롤 (즉각적으로 하단으로)
const scrollToBottom = useCallback(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop =
scrollContainerRef.current.scrollHeight;
}
}, []);
// 메시지 변경 시 자동 스크롤
useEffect(() => {
const timeoutId = setTimeout(() => {
scrollToBottom();
}, 0);
return () => clearTimeout(timeoutId);
}, [messages, streamedText]);
const handleSubmit = () => {
setMessages((prev) => [...prev, { role: "user", content: userInput }]);
setUserInput("");
startStream(); // SSE 스트리밍 시작
};
return (
<Card>
{/* 메시지 영역 */}
<div ref={scrollContainerRef} className="h-[400px] overflow-y-auto">
{messages.map((msg, idx) => (
<MessageBubble key={idx} message={msg} />
))}
{/* 스트리밍 중인 응답 */}
{isStreaming && streamedText && (
<div className="bg-secondary text-secondary-foreground">
{streamedText}
<span className="animate-pulse">▊</span>
</div>
)}
</div>
{/* 입력 영역 */}
<Input value={userInput} onChange={(e) => setUserInput(e.target.value)} />
<Button onClick={handleSubmit}>전송</Button>
</Card>
);
}
핵심 포인트:
scrollTop = scrollHeight로 안정적인 자동 스크롤 구현setTimeout(..., 0)으로 DOM 업데이트 후 스크롤 보장- 스트리밍 중인 텍스트와 완료된 메시지 분리 표시
프로덕션 체크리스트
실제 서비스에 배포할 때 확인해야 할 사항들이다.
필수 설정:
- ✅ HTTP/2 활성화 (Vercel, Netlify는 기본 지원)
- ✅ 하트비트 구현 (30초 주기로
: heartbeat\n\n전송) - ✅ 연결 정리 (클라이언트 disconnect 시 리소스 해제)
- ✅ 에러 처리 (재연결 로직 및 에러 UI)
- ✅ HTTPS 필수 (보안 및 HTTP/2 요구사항)
다중 서버 환경:
Sticky Sessions으로 로드밸런서에서 동일 서버로 라우팅하거나, Redis Pub/Sub으로 서버 간 이벤트를 동기화한다.
성능 최적화:
- 압축:
Accept-Encoding: gzip지원 - 연결 수 제한: 사용자당 최대 연결 수 제한
- 타임아웃: 비활성 연결 자동 종료 (5분)
마치며
SSE는 HTTP를 기반으로 한 단순하지만 강력한 실시간 통신 기술이다. 자동 재연결, 이벤트 ID 기반 복구, HTTP/2와의 완벽한 조합으로 복잡한 WebSocket 없이도 충분히 강력한 실시간 기능을 구현할 수 있다.
단방향 데이터 푸시가 필요한 대부분의 경우, SSE는 가장 실용적인 선택이다.