본문 바로가기
C.W.K.
Stream
Lesson 04 of 06 · published

SSE 스트리밍 — 토큰 단위로 브라우저까지

~15 min · sse, streaming

Level 0호기심
0 XP0/69 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

WebSocket 대신 SSE 인 이유

SSE (Server-Sent Events) 는 단방향: server push, client listen. WebSocket 은 양방향. client 가 prompt 하나 보내고 server 가 token 을 stream 하는 chat 에선 SSE 가 정답 모양 — 더 단순, HTTP middleware 와 잘 어울림, reconnect 무료.

wire format

SSE 는 HTTP 위 text/event-stream 일 뿐. 각 event 는 data: <payload>\n\n. browser 의 EventSource API 또는 fetch + ReadableStream reader 가 parse.

Async 제너레이터 → SSE

FastAPI 의 StreamingResponse 가 어떤 iterable 든 받음. async 제너레이터가 string yield, FastAPI 가 각각을 network 로 flush. event loop 는 yield 사이 다른 작업 자유.

Write before show: browser 로 yield 되는 모든 chunk 가 이미 JSONL ground truth 에 영속화돼야 해. JSONL append 실패 시 yield 안 해. 이게 '아빠한테 깨진 채팅 보여주지 마' 의 코드 표현 (Truth track, lesson 1).

여기서 reconnect를 공짜라고 오해하면 안 돼. transport가 다시 붙는 것과 application state가 이어지는 건 다른 문제야. client는 마지막으로 완전히 처리한 event가 뭔지 알아야 하고, server는 중복 delivery가 와도 같은 chunk를 두 번 확정하지 않게 설계해야 해. stream은 길게 열린 HTTP response일 뿐, 기억까지 대신해 주진 않아.

또 한 가지. token 하나씩 바로 그린다고 항상 UX가 좋아지는 건 아니야. 너무 잘게 render하면 layout과 Markdown parser가 매 token마다 흔들려. wire에서는 세밀하게 받아도 UI에서는 짧게 batch해서 그릴 수 있어. durability boundary, transport boundary, presentation cadence를 같은 것으로 뭉개지 마. 셋은 서로 다른 책임이야.

사실 SSE의 장점은 기능이 적다는 데 있어. 이 흐름에 client-to-server 실시간 제어나 다중 channel multiplexing이 정말 필요해질 때만 WebSocket을 검토해. 이름이 더 강해 보여서 protocol을 키우면, heartbeat부터 reconnect까지 운영 책임도 함께 커져.

Code

Server side — SSE event yield 하는 async generator·python
async def event_stream(req: ChatRequest):
    yield f"data: {json.dumps({'type':'session','id':session_id})}\n\n"
    async for chunk in adapter.stream(req.prompt):
        # eager append to JSONL ground truth — must succeed before yield
        await jsonl_logger.append(conversation_id, chunk)
        yield f"data: {json.dumps(chunk)}\n\n"
    yield f"data: {json.dumps({'type':'done'})}\n\n"
Client side — ReadableStream parser (TypeScript)·ts
async function* parseSSE(response: Response): AsyncGenerator<SSEEvent> {
  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  let buf = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    const lines = buf.split('\n\n');
    buf = lines.pop()!;
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        yield JSON.parse(line.slice(6));
      }
    }
  }
}

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.