SSE
Server-Sent Events
서버에서 클라이언트로 단방향 실시간 데이터를 전송하는 웹 기술. HTTP 기반의 WebSocket 대안으로, EventSource API를 통해 실시간 알림, 피드, 주가 업데이트 등을 구현합니다.
Server-Sent Events
서버에서 클라이언트로 단방향 실시간 데이터를 전송하는 웹 기술. HTTP 기반의 WebSocket 대안으로, EventSource API를 통해 실시간 알림, 피드, 주가 업데이트 등을 구현합니다.
SSE(Server-Sent Events)는 서버가 클라이언트(브라우저)에게 단방향으로 실시간 데이터를 푸시할 수 있는 웹 표준 기술입니다. HTTP 프로토콜을 기반으로 작동하며, 서버에서 클라이언트로만 데이터가 흐르는 "단방향 통신"이 특징입니다. 2004년 Opera 브라우저에서 처음 구현되었고, 이후 HTML5 표준에 포함되었습니다.
SSE는 EventSource API를 통해 브라우저에서 쉽게 사용할 수 있습니다. WebSocket과 달리 별도의 프로토콜 핸드셰이크 없이 일반 HTTP로 작동하므로, 기존 인프라(로드밸런서, 프록시, 방화벽)와 완벽하게 호환됩니다. 연결이 끊어져도 브라우저가 자동으로 재연결을 시도하며, 마지막 이벤트 ID를 통해 놓친 데이터를 복구할 수 있습니다.
SSE의 메시지 형식은 매우 단순합니다: "data:", "event:", "id:", "retry:" 필드로 구성된 텍스트 스트림입니다. Content-Type은 "text/event-stream"을 사용합니다. 이 단순함 덕분에 서버 구현이 쉽고, 어떤 프로그래밍 언어로도 손쉽게 SSE 서버를 만들 수 있습니다.
실제 서비스에서 SSE는 실시간 알림(Facebook, Twitter), 주가/환율 업데이트, 스포츠 경기 실황, ChatGPT와 같은 AI 응답 스트리밍, CI/CD 빌드 로그, 뉴스 피드 등에 널리 사용됩니다. WebSocket이 양방향 통신에 적합하다면, SSE는 서버 푸시만 필요한 대부분의 실시간 시나리오에서 더 간단하고 효율적인 선택입니다.
| 항목 | SSE | WebSocket |
|---|---|---|
| 통신 방향 | 단방향 (서버 → 클라이언트) | 양방향 (서버 ↔ 클라이언트) |
| 프로토콜 | HTTP/HTTPS | WS/WSS (독자 프로토콜) |
| 자동 재연결 | 브라우저 내장 지원 | 직접 구현 필요 |
| 데이터 형식 | 텍스트만 (UTF-8) | 텍스트 + 바이너리 |
| 인프라 호환성 | 높음 (HTTP 기반) | 별도 설정 필요할 수 있음 |
| 적합한 용도 | 알림, 피드, AI 스트리밍 | 채팅, 게임, 실시간 협업 |
// SSE 클라이언트 - EventSource API
const eventSource = new EventSource('/api/events');
// 기본 메시지 수신
eventSource.onmessage = (event) => {
console.log('데이터 수신:', event.data);
const data = JSON.parse(event.data);
updateUI(data);
};
// 연결 성공
eventSource.onopen = () => {
console.log('SSE 연결 성공!');
};
// 에러 처리 (자동 재연결됨)
eventSource.onerror = (error) => {
console.log('SSE 에러, 재연결 시도 중...');
if (eventSource.readyState === EventSource.CLOSED) {
console.log('연결 종료됨');
}
};
// 커스텀 이벤트 타입 수신
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification.title, notification.body);
});
eventSource.addEventListener('price-update', (event) => {
const price = JSON.parse(event.data);
updateStockPrice(price.symbol, price.value);
});
// 연결 종료 (컴포넌트 언마운트 시)
function cleanup() {
eventSource.close();
console.log('SSE 연결 종료');
}
// React/Vue에서 사용 예시
// useEffect(() => {
// const es = new EventSource('/api/events');
// es.onmessage = handleMessage;
// return () => es.close(); // cleanup
// }, []);
// Express.js SSE 서버
const express = require('express');
const app = express();
app.get('/api/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', '*');
// 연결 유지를 위한 heartbeat
const heartbeat = setInterval(() => {
res.write(':heartbeat\n\n');
}, 30000);
// 클라이언트 ID 추적
const clientId = Date.now();
console.log(`클라이언트 연결: ${clientId}`);
// 데이터 전송 함수
function sendEvent(data, eventType = null, id = null) {
if (id) res.write(`id: ${id}\n`);
if (eventType) res.write(`event: ${eventType}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// 초기 데이터 전송
sendEvent({ message: '연결 성공!', clientId }, 'connected');
// 예시: 1초마다 시간 전송
const interval = setInterval(() => {
sendEvent({
time: new Date().toISOString(),
random: Math.random()
});
}, 1000);
// 연결 종료 처리
req.on('close', () => {
clearInterval(interval);
clearInterval(heartbeat);
console.log(`클라이언트 연결 해제: ${clientId}`);
});
});
// 여러 클라이언트에게 브로드캐스트
const clients = new Set();
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
clients.add(res);
req.on('close', () => clients.delete(res));
});
function broadcast(data) {
clients.forEach(client => {
client.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
app.listen(3000);
# FastAPI SSE 서버
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse
import asyncio
import json
app = FastAPI()
# 간단한 SSE 엔드포인트
@app.get("/api/events")
async def events():
async def event_generator():
while True:
# 데이터 생성
data = {
"timestamp": str(asyncio.get_event_loop().time()),
"message": "Hello SSE!"
}
yield f"data: {json.dumps(data)}\n\n"
await asyncio.sleep(1)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
# sse-starlette 라이브러리 사용 (권장)
# pip install sse-starlette
@app.get("/api/notifications")
async def notifications(request: Request):
async def event_stream():
message_id = 0
while True:
if await request.is_disconnected():
break
message_id += 1
yield {
"event": "notification",
"id": str(message_id),
"retry": 5000, # 재연결 대기 시간 (ms)
"data": json.dumps({
"title": "새 알림",
"body": f"메시지 #{message_id}"
})
}
await asyncio.sleep(2)
return EventSourceResponse(event_stream())
# ChatGPT 스타일 AI 스트리밍 응답
@app.get("/api/chat")
async def chat_stream(prompt: str):
async def generate_response():
# AI 모델 응답 시뮬레이션
response = "안녕하세요! SSE를 사용한 스트리밍 응답입니다."
for char in response:
yield f"data: {json.dumps({'token': char})}\n\n"
await asyncio.sleep(0.05) # 타이핑 효과
yield f"data: {json.dumps({'done': True})}\n\n"
return StreamingResponse(
generate_response(),
media_type="text/event-stream"
)
"실시간 알림 기능은 SSE로 구현하면 될 것 같습니다. 서버에서 클라이언트로만 푸시하면 되니까 WebSocket까지 쓸 필요는 없고요. HTTP 기반이라 로드밸런서 설정도 건드릴 필요 없어요. EventSource API가 자동 재연결도 해주니까 클라이언트 코드도 간단합니다."
"SSE는 HTTP 기반의 단방향 실시간 통신 기술입니다. WebSocket과 비교하면, SSE는 서버 → 클라이언트 방향만 지원하지만 구현이 단순하고 인프라 호환성이 좋습니다. ChatGPT가 답변을 스트리밍하는 것도 SSE를 사용하고요. 브라우저가 자동 재연결을 지원해서 네트워크 불안정 환경에서도 안정적입니다."
"SSE 연결 시 heartbeat를 30초마다 보내는 게 좋을 것 같아요. 프록시나 로드밸런서가 유휴 연결을 끊을 수 있거든요. 그리고 event ID를 설정해두면 재연결 시 Last-Event-ID 헤더로 놓친 이벤트를 복구할 수 있습니다."
HTTP/1.1에서 브라우저는 도메인당 6개 연결만 허용합니다. SSE 탭을 여러 개 열면 다른 요청이 블로킹될 수 있습니다. HTTP/2를 사용하거나 연결을 공유하세요.
프록시, 로드밸런서, 방화벽이 유휴 연결을 30~60초 후 끊을 수 있습니다. 15~30초마다 코멘트(:heartbeat)나 빈 이벤트를 전송하세요.
Nginx, Apache 등이 응답을 버퍼링하면 SSE가 작동하지 않습니다. X-Accel-Buffering: no 헤더를 추가하거나 proxy_buffering off 설정이 필요합니다.
retry 필드로 재연결 간격 설정, id 필드로 이벤트 추적, 연결 해제 시 리소스 정리, 적절한 에러 처리와 재연결 로직을 구현하세요.