Server-Sent Events (SSE) 사용법: 완벽 가이드 (2026)
서버에서 클라이언트로의 실시간 스트리밍 설명
Hypereal로 구축 시작하기
단일 API를 통해 Kling, Flux, Sora, Veo 등에 액세스하세요. 무료 크레딧으로 시작하고 수백만으로 확장하세요.
신용카드 불필요 • 10만 명 이상의 개발자 • 엔터프라이즈 지원
Server-Sent Events (SSE) 사용법: 완벽 가이드 (2026)
Server-Sent Events (SSE)는 서버가 단일 HTTP 연결을 통해 실시간 업데이트를 웹 클라이언트에 푸시할 수 있는 단순하고 표준화된 방법을 제공합니다. WebSockets와 달리 SSE는 단방향(서버에서 클라이언트로만 가능)이며, 일반 HTTP를 사용하고, 특별한 설정 없이 프록시와 방화벽을 통과하며, 연결이 끊어지면 자동으로 재연결됩니다. 이러한 점 덕분에 SSE는 라이브 피드, AI 토큰 스트리밍, 알림 및 대시보드에 이상적인 선택입니다.
SSE vs WebSockets vs Long Polling
구현에 앞서 SSE가 적절한 선택인 상황을 이해하는 것이 도움이 됩니다.
| 기능 | SSE | WebSocket | Long Polling |
|---|---|---|---|
| 방향성 | 서버에서 클라이언트로 | 양방향 | 서버에서 클라이언트로 |
| 프로토콜 | HTTP | WS/WSS | HTTP |
| 자동 재연결 | 내장됨 | 수동 구현 | 수동 구현 |
| 바이너리 데이터 | 불가 (텍스트 전용) | 가능 | 가능 |
| 브라우저 지원 | 모든 현대적 브라우저 | 모든 현대적 브라우저 | 모든 브라우저 |
| 프록시 친화성 | 높음 | 때때로 문제 발생 | 높음 |
| 연결 오버헤드 | 낮음 (단일 HTTP) | 낮음 (단일 TCP) | 높음 (반복적인 HTTP) |
| 최적의 용도 | 피드, 알림, AI 스트리밍 | 채팅, 게임, 협업 도구 | 레거시 호환성 |
SSE를 사용해야 할 때: 서버-클라이언트 단방향 업데이트만 필요하고, 자동 재연결 기능이 필요하며, 양방향 통신보다 단순함을 선호하는 경우.
SSE의 동작 원리
SSE 프로토콜은 매우 간단합니다:
- 클라이언트가
Accept: text/event-stream헤더와 함께 표준 HTTP GET 요청을 보냅니다. - 서버는
Content-Type: text/event-stream으로 응답하고 연결을 열린 상태로 유지합니다. - 서버는 두 개의 줄바꿈으로 구분된 일반 텍스트 이벤트를 전송합니다.
- 연결이 끊어지면 브라우저가 자동으로 재연결을 시도합니다.
SSE 메시지 형식
각 SSE 메시지는 하나 이상의 필드로 구성되며, 각 필드는 개별 줄에 위치합니다:
event: message_type
id: unique_id_123
retry: 5000
data: {"text": "Hello, world"}
필드 설명:
data:-- 메시지 페이로드 (필수). 여러 개의data:줄은 줄바꿈으로 병합됩니다.event:-- 명명된 이벤트 타입 (선택 사항). 기본값은"message"입니다.id:-- 고유 이벤트 ID (선택 사항). 재연결 시 사용됩니다.retry:-- 밀리초 단위의 재연결 간격 (선택 사항).
빈 줄(이중 줄바꿈 \n\n)은 이벤트의 끝을 나타냅니다.
서버 구현
Node.js (Express)
const express = require("express");
const app = express();
app.get("/events", (req, res) => {
// SSE 헤더 설정
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
// 즉시 헤더 플러시
res.flushHeaders();
// 2초마다 이벤트 전송
let counter = 0;
const interval = setInterval(() => {
counter++;
const data = JSON.stringify({
time: new Date().toISOString(),
count: counter
});
res.write(`id: ${counter}\n`);
res.write(`event: tick\n`);
res.write(`data: ${data}\n\n`);
}, 2000);
// 클라이언트 연결 종료 시 정리
req.on("close", () => {
clearInterval(interval);
res.end();
console.log("Client disconnected");
});
});
app.listen(3000, () => {
console.log("SSE server running on http://localhost:3000");
});
Python (FastAPI)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json
from datetime import datetime
app = FastAPI()
async def event_generator():
counter = 0
while True:
counter += 1
data = json.dumps({
"time": datetime.now().isoformat(),
"count": counter
})
yield f"id: {counter}\nevent: tick\ndata: {data}\n\n"
await asyncio.sleep(2)
@app.get("/events")
async def stream_events():
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
Python (Flask)
from flask import Flask, Response
import json
import time
from datetime import datetime
app = Flask(__name__)
def generate_events():
counter = 0
while True:
counter += 1
data = json.dumps({
"time": datetime.now().isoformat(),
"count": counter
})
yield f"id: {counter}\nevent: tick\ndata: {data}\n\n"
time.sleep(2)
@app.route("/events")
def stream():
return Response(
generate_events(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # nginx 버퍼링 비활성화
}
)
클라이언트 구현
브라우저 (EventSource API)
브라우저는 연결 관리, 자동 재연결, 이벤트 파싱을 처리하는 EventSource API를 내장하고 있습니다:
// SSE 엔드포인트에 연결
const eventSource = new EventSource("http://localhost:3000/events");
// 기본 "message" 이벤트 리스닝
eventSource.onmessage = (event) => {
console.log("Message:", event.data);
};
// 명명된 이벤트 리스닝
eventSource.addEventListener("tick", (event) => {
const data = JSON.parse(event.data);
console.log(`Tick #${data.count} at ${data.time}`);
});
// 연결 성공 시 처리
eventSource.onopen = () => {
console.log("Connection established");
};
// 오류 및 재연결 처리
eventSource.onerror = (error) => {
console.error("SSE error:", error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log("Connection was closed");
} else {
console.log("Reconnecting...");
}
};
// 완료 후 연결 종료
// eventSource.close();
SSE용 React Hook
import { useEffect, useState, useCallback } from "react";
interface SSEOptions {
url: string;
eventName?: string;
onError?: (error: Event) => void;
}
function useSSE<T>(options: SSEOptions) {
const [data, setData] = useState<T | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(options.url);
eventSource.onopen = () => setIsConnected(true);
eventSource.onerror = (error) => {
setIsConnected(false);
options.onError?.(error);
};
const handler = (event: MessageEvent) => {
try {
const parsed = JSON.parse(event.data) as T;
setData(parsed);
} catch {
setData(event.data as unknown as T);
}
};
if (options.eventName) {
eventSource.addEventListener(options.eventName, handler);
} else {
eventSource.onmessage = handler;
}
return () => {
eventSource.close();
};
}, [options.url, options.eventName]);
return { data, isConnected };
}
// 컴포넌트에서의 사용 예시
function LiveDashboard() {
const { data, isConnected } = useSSE<{ time: string; count: number }>({
url: "/events",
eventName: "tick",
});
return (
<div>
<p>상태: {isConnected ? "연결됨" : "재연결 중..."}</p>
{data && (
<p>카운트: {data.count} | 시간: {data.time}</p>
)}
</div>
);
}
실제 활용 사례: AI 토큰 스트리밍
2026년 기준 SSE의 가장 일반적인 용도 중 하나는 LLM API에서 생성된 AI 토큰을 스트리밍하는 것입니다. 구현 방법은 다음과 같습니다:
서버 (AI API 프록시)
app.post("/api/chat", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
try {
const aiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-4o",
messages: req.body.messages,
stream: true
})
});
const reader = aiResponse.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// SSE 데이터를 직접 전달
res.write(chunk);
}
res.write("data: [DONE]\n\n");
res.end();
} catch (error) {
res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
클라이언트 (fetch를 이용한 스트리밍)
POST 요청(EventSource가 지원하지 않음)의 경우, Readable Stream과 함께 Fetch API를 사용합니다:
async function streamChat(messages) {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") return;
try {
const parsed = JSON.parse(data);
const token = parsed.choices?.[0]?.delta?.content;
if (token) {
document.getElementById("output").textContent += token;
}
} catch {
// JSON이 아닌 줄은 건너뜀
}
}
}
}
}
일반적인 문제 해결 (Troubleshooting)
| 문제점 | 원인 | 해결책 |
|---|---|---|
| 이벤트가 뭉쳐서 도착함 | 프록시 버퍼링 (nginx, CloudFlare) | X-Accel-Buffering: no 헤더 추가; 프록시 버퍼링 비활성화 |
| 60초 후 연결 끊김 | 서버 또는 프록시 타임아웃 | 30초마다 주석 줄(:keepalive\n\n) 전송 |
| 자동 재연결이 안 됨 | EventSource 대신 fetch 사용 |
GET 요청에는 EventSource 사용; POST에는 수동 재시도 구현 |
| CORS 오류 | 헤더 누락 | 서버에 Access-Control-Allow-Origin 추가 |
| 재연결 시 중복 이벤트 발생 | 이벤트 ID 없음 | id: 필드 포함; 서버에서 Last-Event-ID 헤더 추적 |
Keep-Alive 패턴
프록시 타임아웃을 방지하려면 주기적으로 주석 줄을 전송하십시오:
// 서버측 keep-alive
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
req.on("close", () => {
clearInterval(keepAlive);
});
모범 사례 (Best Practices)
- 항상 이벤트 ID를 포함하여 클라이언트가 재연결 후 중단된 지점부터 다시 시작할 수 있도록 하세요.
retry:필드를 사용하여 적절한 재시도 간격을 설정하세요 (기본값은 보통 3초입니다).- 프록시 타임아웃 방지를 위해 15~30초마다 Keep-alive 주석을 전송하세요.
- 동일한 스트림 내의 서로 다른 메시지 유형을 구분하기 위해 명명된 이벤트(Named events)를 사용하세요.
- 데이터를 쓰기 전에 클라이언트가 여전히 연결되어 있는지 확인하여 백프레셔(Backpressure)를 처리하세요.
- 리소스 고갈을 방지하기 위해 사용자당 동시 연결 수를 제한하세요.
결론
Server-Sent Events는 실시간 서버-클라이언트 통신을 위한 깔끔하고 표준화된 접근 방식을 제공합니다. 단방향 스트리밍의 경우 WebSockets보다 훨씬 간단하며, 브라우저의 기본 EventSource API가 자동 재연결을 처리해 줍니다. AI 스트리밍 API가 부상함에 따라 SSE는 모든 개발자의 도구함에 필수적인 도구가 되었습니다.
실시간 비디오 생성 상태 업데이트, 점진적 이미지 렌더링 또는 라이브 전사 피드와 같이 AI 생성 콘텐츠를 스트리밍하는 애플리케이션을 구축 중이라면, Hypereal AI는 생성 진행 상황 추적을 위한 내장 SSE 지원과 함께 비디오 생성, 말하는 아바타 및 이미지 합성을 위한 스트리밍 호환 API를 제공합니다.
