TL;DR
- SSE๋ HTTP ์์์ ์๋ฒโํด๋ผ์ด์ธํธ ๋จ๋ฐฉํฅ ์ด๋ฒคํธ ํธ์๋ฅผ ๊ตฌํํ๋ ํ๋กํ ์ฝ์ด๋ค.
text/event-stream๋ฏธ๋์ด ํ์ , ํ๋ ๊ธฐ๋ฐ ๋ฉ์์ง ํฌ๋งท(data:,event:,id:,retry:), ๋น ์ค(\n\n)๋ก ๋ฉ์์ง ๊ฒฝ๊ณ๋ฅผ ๊ตฌ๋ถํ๋ค.
SSE (Server-Sent Events)
๋ฐฐ๊ฒฝ
์ ํต์ ์ธ HTTP๋ ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํด์ผ ์๋ฒ๊ฐ ์๋ตํ๋ ๊ตฌ์กฐ๋ค. SSE๋ ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ํธ์ํ ์ ์๊ฒ ํด์ฃผ๋ ํ์ค์ผ๋ก, ํด๋ง ์์ด ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ค. WHATWG HTML ์คํ์ ์ผ๋ถ๋ก ์ ์๋์ด ์๋ค.
๋ฉ์์ง ํฌ๋งท
SSE ์คํธ๋ฆผ์ ํ๋ ๊ธฐ๋ฐ ํ
์คํธ ํฌ๋งท์ด๋ค. ๊ฐ ์ค์ field: value ํํ์ด๋ฉฐ, ๋น ์ค(\n\n)์ด ๋ฉ์์ง ๊ฒฝ๊ณ๋ฅผ ๋ํ๋ธ๋ค.
ํ๋ ์ข ๋ฅ
| ํ๋ | ์ญํ |
|---|---|
data: | ๋ฉ์์ง ํ์ด๋ก๋. ์ฌ๋ฌ ์ค์ด๋ฉด \n์ผ๋ก ์ฐ๊ฒฐ |
event: | ์ด๋ฒคํธ ํ์
(์๋ต ์ "message") |
id: | ์ด๋ฒคํธ ID. ์ฌ์ฐ๊ฒฐ ์ Last-Event-ID ํค๋๋ก ์ ์ก |
retry: | ์ฌ์ฐ๊ฒฐ ๋๊ธฐ ์๊ฐ (๋ฐ๋ฆฌ์ด, ASCII ์ซ์๋ง) |
: ๋ก ์์ํ๋ ์ค์ ์ฝ๋ฉํธ๋ก ๋ฌด์๋๋ค (keep-alive ์ฉ๋๋ก ํ์ฉ).
์์
event: update
data: {"score": 42}
id: 7
event: update
data: {"score": 43}
id: 8
ํต์ฌ ํน์ฑ
- ํ๋กํ ์ฝ: HTTP ์ ์ฉ (๋จ๋ฐฉํฅ, ์๋ฒโํด๋ผ์ด์ธํธ)
- ๋ฏธ๋์ด ํ์
:
text/event-stream - ์ธ์ฝ๋ฉ: UTF-8 ํ์
- ์ค ๋:
\n,\r\n,\r๋ชจ๋ ํ์ฉ - ๋ฉ์์ง ๊ตฌ๋ถ: ๋น ์ค (
\n\n) - ์ฌ์ฐ๊ฒฐ: ์ฐ๊ฒฐ ๋๊ธฐ๋ฉด ์๋ ์ฌ์ฐ๊ฒฐ.
id:ํ๋๊ฐ ์์ผ๋ฉดLast-Event-IDํค๋๋ก ์ด์ด๋ฐ๊ธฐ ๊ฐ๋ฅ
์๋ฒ ๊ตฌํ (FastAPI)
sse-starlette์ EventSourceResponse๋ฅผ ์ฌ์ฉํ๋ฉด SSE ํฌ๋งทํ
์ ์ง์ ํ์ง ์์๋ ๋๋ค:
from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse
import asyncio, json
app = FastAPI()
async def event_generator():
for i in range(10):
yield {"event": "update", "id": str(i), "data": json.dumps({"score": i})}
await asyncio.sleep(1)
@app.get("/stream")
async def stream():
return EventSourceResponse(event_generator())EventSourceResponse๊ฐ text/event-stream ํค๋, data:/event:/id: ํ๋ ํฌ๋งทํ
, \n\n ๊ตฌ๋ถ์ ๋ชจ๋ ์ฒ๋ฆฌํ๋ค.
์ง์ ๊ตฌํ
sse-starlette์์ด๋StreamingResponse(generator, media_type="text/event-stream")์ผ๋ก ์ง์ f"data: {json}\n\n"๋ฌธ์์ด์ yieldํ๋ ๋ฐฉ์๋ ๊ฐ๋ฅํ๋ค.
ํด๋ผ์ด์ธํธ (EventSource API)
๋ธ๋ผ์ฐ์ ์์ SSE๋ฅผ ์๋นํ๋ ํ์ค JavaScript API:
const source = new EventSource('/stream');
source.addEventListener('update', (e) => console.log(JSON.parse(e.data)));
source.addEventListener('error', (e) => console.error(e));์ด๋ฒคํธ: open (์ฐ๊ฒฐ ์๋ฆฝ), message (๊ธฐ๋ณธ ์ด๋ฒคํธ), error (์ฐ๊ฒฐ ์คํจ). ์ปค์คํ
event: ํ๋๋ฅผ ์ฐ๋ฉด ํด๋น ์ด๋ฒคํธ๋ช
์ผ๋ก ๋ฆฌ์ค๋ํด์ผ ํ๋ค.
ํ์ฉ ๋งฅ๋ฝ
- LLM ์คํธ๋ฆฌ๋ฐ ์๋ต: OpenAI, Anthropic ๋ฑ LLM API๊ฐ ํ ํฐ ๋จ์ ์คํธ๋ฆฌ๋ฐ์ SSE ์ฌ์ฉ
- ์ค์๊ฐ ๋์๋ณด๋: ์ฃผ๊ฐ, ์ค์ฝ์ด๋ณด๋, ์๋ฆผ ๋ฑ ์๋ฒ ํธ์
- CI/CD ๋ก๊ทธ ์คํธ๋ฆฌ๋ฐ: ๋น๋ ๋ก๊ทธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ธ๋ผ์ฐ์ ์ ์ ๋ฌ
Connections
- NDJSON (Newline Delimited JSON) โ ๋ ๋ค ์ค ๋จ์ ์คํธ๋ฆฌ๋ฐ์ด์ง๋ง, NDJSON์ ํ๋กํ ์ฝ ๋ฌด๊ด ์์ JSON ์ง๋ ฌํ ํฌ๋งท์ด๊ณ SSE๋ HTTP ์ด๋ฒคํธ ์๋งจํฑ(id, retry, event type) ๋ด์ฅ
- WebSocket โ SSE๋ ๋จ๋ฐฉํฅ(์๋ฒโํด๋ผ์ด์ธํธ), WebSocket์ ์๋ฐฉํฅ
Source Trail
- ์๋ณธ ์คํ: WHATWG HTML โ Server-Sent Events
Discussion
Comments
๋๊ธ์ ์น์ธ ํ ๊ณต๊ฐ๋ฉ๋๋ค.