핵심 요약
SSE는 서버 → 클라이언트 단방향 HTTP 스트리밍이다. text/event-stream MIME 타입의 응답을 끊지 않고 이벤트를 push한다. 2026년 현재 ChatGPT·Claude·GitHub 알림 등 주요 서비스 상당수가 SSE를 쓴다.
- 프로토콜: 표준 HTTP(S), 추가 설치 불필요
- 브라우저 API:
EventSource내장 - 자동 재연결: 표준 스펙 포함
- 단방향: 클라이언트 → 서버는 일반 AJAX로 처리
SSE vs WebSocket — 언제 무엇을 쓰나
| 항목 | SSE | WebSocket |
|---|---|---|
| 방향 | 서버 → 클라이언트 | 양방향 |
| 프로토콜 | HTTP | Upgrade → ws |
| 프록시·CDN 호환 | 최고 | 설정 필요 |
| 자동 재연결 | 기본 포함 | 직접 구현 |
| 브라우저 호환 | 99%+ | 99%+ |
| 초당 이벤트 | 수백 ~ 수천 | 수만+ |
LLM 스트리밍 응답·실시간 알림·대시보드 업데이트·빌드 로그 스트리밍은 SSE가 최적. 채팅·게임·실시간 협업 에디터는 WebSocket.
클라이언트 (브라우저)
const es = new EventSource('/api/stream');
es.onmessage = (e) => console.log('default:', e.data);
es.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
updateProgress(data.percent);
});
es.addEventListener('done', () => es.close());
es.onerror = () => console.log('자동 재연결 중...');
서버 — Node/Express
app.get('/api/stream', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Nginx buffering 비활성
});
res.flushHeaders();
// keepalive ping 15초
const ping = setInterval(() => res.write(':ping\n\n'), 15000);
const send = (event, data) => {
if (event) res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
send('start', { ts: Date.now() });
// 이벤트 소스
const unsubscribe = eventBus.subscribe((evt) => send(evt.type, evt.payload));
req.on('close', () => { clearInterval(ping); unsubscribe(); });
});
서버 — Go
func stream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, _ := w.(http.Flusher)
ctx := r.Context()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case t := <-ticker.C:
fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
flusher.Flush()
}
}
}
프록시 설정 (매우 중요)
Nginx
location /api/stream {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off; # 핵심
proxy_cache off;
proxy_read_timeout 24h;
chunked_transfer_encoding on;
}
Cloudflare
기본적으로 SSE 지원. 단 "Caching Level: Bypass"와 proxy_buffering 끄기를 오리진에서 확실히 해야 한다. 클플은 응답을 캐싱하지 않지만 일부 프리페치 레이어가 스트리밍을 방해할 수 있다.
자동 재연결과 Last-Event-ID
클라이언트가 연결이 끊기면 자동으로 재연결한다. 서버가 id: 필드를 보냈다면 재연결 요청에 Last-Event-ID 헤더가 포함된다. 서버는 그 지점부터 재개.
res.write(`id: ${msgId}\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
한계·주의
- HTTP/1.1에서는 브라우저당 도메인 연결 6개 제한에 걸릴 수 있음 → HTTP/2·3에서 해결
- 텍스트 기반 → 바이너리 필요하면 base64 오버헤드
- 클라이언트 → 서버는 별도 REST/GraphQL 호출 필요
- 서버 메모리: 연결당 소량이지만 10만 연결 시 수 GB 가능 — Node는 cluster/worker 튜닝 필요
실전 유스케이스
- LLM 응답 토큰 단위 스트리밍
- CI 빌드 로그 실시간 전송
- 주식·암호화폐 실시간 시세
- Notion·Linear 같은 도구의 문서 동기화 알림
- Slack·Discord 등의 알림 배지 업데이트
자주 묻는 질문
HTTPS에서만 동작하나?
HTTP에서도 가능. 다만 HTTP/2 이상에서 연결 수 제한이 해결돼 운영상 HTTPS(HTTP/2+)가 사실상 표준.
React Native·모바일 지원은?
네이티브 EventSource가 없으면 fetch ReadableStream으로 구현하는 라이브러리를 쓰면 된다. iOS/Android 모두 동작.
keepalive ping은 꼭 필요한가?
프록시·방화벽의 idle timeout(30~60초)을 막기 위해 15초 간격 comment(:ping)를 보내는 것이 권장이다.
댓글 0