3.8 KiB
3.8 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| web-sse-server-sent-events | SSE — 단방향 스트리밍 (LLM, 알림) | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
SSE — Server-Sent Events
단방향 (서버 → 클라이언트) 스트리밍. WebSocket 보다 단순. LLM 토큰 스트리밍 / 알림 / 진행률에 적합. 자동 재연결 내장. 단 헤더 / 쿼리스트링 인증만 (커스텀 헤더 X — fetch 기반 SSE 라이브러리로 우회).
📖 핵심 개념
- HTTP long-lived connection.
- 응답
Content-Type: text/event-stream. data: <json>\n\n단위 메시지.- 자동 reconnect —
Last-Event-ID로 resume.
💻 코드 패턴
서버 (Express)
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // nginx buffer 끄기
let id = Number(req.header('last-event-id') ?? 0);
const send = (event: string, data: any) => {
res.write(`id: ${++id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
};
const interval = setInterval(() => send('tick', { time: Date.now() }), 1000);
req.on('close', () => clearInterval(interval));
});
클라이언트 — EventSource
const es = new EventSource('/api/stream', { withCredentials: true });
es.addEventListener('tick', e => {
const data = JSON.parse(e.data);
console.log('tick', data.time);
});
es.onerror = () => {
// 자동 재연결됨. 5xx 면 reconnect, 401 이면 close.
};
// 종료
es.close();
LLM 토큰 스트리밍
// 서버
app.post('/api/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
for await (const chunk of llm.stream(req.body.prompt)) {
res.write(`data: ${JSON.stringify({ token: chunk })}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
});
// 클라이언트 — fetch + 스트림 (헤더 보내야 할 때)
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ prompt }),
onmessage(ev) {
if (ev.data === '[DONE]') return;
const { token } = JSON.parse(ev.data);
setText(t => t + token);
},
onerror(err) { throw err; }, // 재연결 안 함
});
🤔 의사결정 기준
| 상황 | SSE | WebSocket | Polling |
|---|---|---|---|
| 단방향 (서버 → 클) | ✅ | ✅ | △ |
| 양방향 | ❌ | ✅ | — |
| LLM 응답 스트림 | ✅ | OK | ❌ |
| 진행률 / status | ✅ | OK | ❌ |
| 자동 재연결 필요 | ✅ (내장) | 직접 | — |
| HTTP/2 다중화 | ✅ | ❌ | — |
| 커스텀 헤더 / cookie 외 인증 | fetchEventSource 라이브러리 | ✅ | ✅ |
❌ 안티패턴
- proxy buffer 켬: 메시지 지연. nginx
proxy_buffering off;,X-Accel-Buffering: no. - CORS 응답 헤더 누락: 다른 origin EventSource 차단.
- Content-Type 잘못 (
application/json): EventSource 거부. - 재연결 무한 루프 + 401: 서버가 401 보내야 close. backoff 있더라도 인증 만료 처리 별도.
- 너무 짧은 keep-alive timeout: 매분 재연결.
- 메시지 \n\n 없이: 클라가 안 받음.
- Last-Event-ID 무시: resume 못 함.
🤖 LLM 활용 힌트
- 신규 LLM streaming = SSE 디폴트.
- 인증 헤더 필요 = fetchEventSource.