Files
2nd/10_Wiki/Topics/Coding/Web_SSE_Server_Sent_Events.md
T
2026-05-09 21:08:02 +09:00

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
web
sse
streaming
vibe-coding
language applicable_to
TypeScript / EventSource
Web
Backend
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)

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.

🔗 관련 문서