[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: web-sse-server-sent-events
|
||||
title: SSE — 단방향 스트리밍 (LLM, 알림)
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [web, sse, streaming, vibe-coding]
|
||||
tech_stack: { language: "TypeScript / EventSource", applicable_to: ["Web", "Backend"] }
|
||||
applied_in: []
|
||||
aliases: [Server-Sent Events, EventSource, text/event-stream, retry]
|
||||
---
|
||||
|
||||
# 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)
|
||||
```ts
|
||||
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
|
||||
```ts
|
||||
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 토큰 스트리밍
|
||||
```ts
|
||||
// 서버
|
||||
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();
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// 클라이언트 — 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.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Web_WebSocket_Reconnect]]
|
||||
- [[AI_Streaming_LLM_Response]]
|
||||
Reference in New Issue
Block a user