TL;DR
- SSE는 HTTP 연결을 유지한 상태로 서버가 클라이언트에 실시간 이벤트를 푸시하는 단방향 스트리밍 프로토콜이다.
text/event-stream미디어 타입과 UTF-8 인코딩을 사용하며, 단순 텍스트 기반 필드 구조를 가져 구현이 가볍다.
SSE (Server-Sent Events)
1. 개요
SSE는 표준 HTTP 프로토콜을 기반으로 서버에서 클라이언트로 데이터를 지속적으로 전송하는 기술이다. 클라이언트가 주기적으로 데이터를 요청하는 폴링(Polling) 방식과 달리, 서버가 이벤트 발생 시마다 데이터를 즉시 푸시하므로 실시간성 확보와 자원 효율성이 높다.
2. 메시지 규격 및 포맷
SSE는 줄바꿈 기호(\n)로 구분되는 텍스트 기반 포맷을 사용한다. 각 메시지는 빈 줄(\n\n) 을 통해 경계가 구분된다.
필드 구조
| 필드명 | 역할 | 상세 설명 |
|---|---|---|
data: | 메시지 페이로드 | 실제 전송할 데이터. 여러 줄일 경우 각 줄마다 data: 접두어를 붙인다. |
event: | 이벤트 타입 | 클라이언트에서 리스닝할 이벤트 명칭 (기본값: message). |
id: | 이벤트 ID | 브라우저가 마지막으로 수신한 ID를 추적하며, 재연결 시 Last-Event-ID 헤더에 실어 보낸다. |
retry: | 재연결 대기 시간 | 연결 종료 시 재시도까지 대기할 밀리초(ms) 단위 시간. |
코멘트 라인
:기호로 시작하는 행은 코멘트로 처리되어 클라이언트에서 무시된다. 주로 연결 유지를 위한 Keep-alive 용도로 활용된다.
3. 기술적 특성
- 프로토콜: HTTP 전용 (단방향: 서버 클라이언트).
- 인코딩: UTF-8 필수. 텍스트 기반이므로 바이너리 데이터는 Base64 인코딩 같은 텍스트 인코딩으로 감싸야 한다.
- 연결 복구: 브라우저의
EventSourceAPI는 자동 재연결을 수행하고, 마지막으로 수신한 id를Last-Event-ID헤더로 서버에 전달한다. 실제 유실 데이터 재전송은 서버가 해당 id를 기준으로 backfill 로직을 구현해야 가능하다. - 제한 사항: HTTP/1.1에서는 브라우저당 최대 6개로 연결이 제한될 수 있으나, HTTP/2 이상에서는 멀티플렉싱을 통해 이 제한을 극복한다.
4. 실무 활용 사례 (LLM Streaming)
최근 OpenAI, Anthropic 등 주요 상용 LLM API는 응답 델타를 점진적으로 전송하기 위해 SSE를 자주 사용한다.
- 서버는 모델 응답이 생성될 때마다
data: {"text": "토큰..."}\n\n형태의 이벤트를 전송할 수 있다. - 일부 API는
data: [DONE]\n\n같은 관습적인 종결 표식을 쓰고, 다른 API는event:이름이나 JSON 내부의type필드로 종료 이벤트를 표현한다.
JSON payload와 NDJSON 구분
LLM 스트리밍에서 주요 상용 API가 흔히 사용하는 구조는 SSE 이벤트의 data: 필드 안에 직렬화된 JSON payload를 담는 방식이다. 이때 전체 응답은 text/event-stream으로 해석되며, 이벤트는 여러 필드 줄로 구성되고 빈 줄로 종료된다. 실무에서는 흔히 \n\n 형태로 보이지만, 명세상 줄 종결자는 \n, \r, \r\n이 모두 가능하다.
SSE 자체는 data: 페이로드의 형식을 정하지 않는다. JSON은 SSE의 규칙이 아니라 API가 선택한 payload 포맷이다. 한 이벤트 안에 data: 줄이 여러 개 있으면 클라이언트는 이를 newline으로 이어 붙여 하나의 이벤트 데이터로 처리한다.
반면 NDJSON (Newline Delimited JSON)은 각 줄 자체가 완전한 JSON 텍스트인 줄 단위 데이터 포맷이다. 순수 NDJSON 스트림이라면 data: 접두어 없이 {"delta":"..."} 같은 JSON 값이 줄마다 바로 나오며, 미디어 타입도 보통 application/x-ndjson을 사용한다.
따라서 data: {"delta":"..."}\n\n 형태는 NDJSON이 아니라 JSON payload를 담은 SSE다. SSE와 NDJSON은 모두 스트리밍에 쓰일 수 있지만, SSE는 전송 프레이밍 규격이고 NDJSON은 줄 단위 데이터 포맷이라는 점에서 서로 다른 레이어에 속한다.
5. 구현 예시
서버 (Python / FastAPI)
sse-starlette 라이브러리를 사용하면 복잡한 포맷팅 없이 비동기 제너레이터를 스트림으로 변환할 수 있다.
from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse
import asyncio, json
app = FastAPI()
async def event_generator():
for i in range(5):
# 1초 간격으로 데이터 푸시
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())클라이언트 (JavaScript / EventSource)
브라우저 표준 API인 EventSource를 사용하여 서버 스트림을 소비한다.
const source = new EventSource('/stream');
// 커스텀 이벤트(event: update) 수신
source.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log("Score updated:", data.score);
});
// 에러 처리 및 자동 재연결 관리
source.onerror = (err) => {
console.error("SSE connection error:", err);
};Connections
- SSE vs NDJSON — 전송 프로토콜(SSE)과 데이터 포맷(NDJSON)의 기술적 차이 및 LLM 스트리밍에서의 SSE + JSON payload 사례 비교
- UTF-8 — SSE 스펙에서 강제하는 표준 문자 인코딩
- ASCII (American Standard Code for Information Interchange) — 줄바꿈(
\n,\r) 및 필드명 식별의 기초 - NDJSON (Newline Delimited JSON) — 줄 단위 JSON 포맷. SSE와 비슷해 보일 수 있지만, SSE의
data:안에는 보통 NDJSON이 아니라 JSON payload가 담긴다. - [[WebSocket]] — 양방향 통신이 필요한 경우의 대안 (SSE는 단방향 전용)
Source Trail
- 공식 스펙: WHATWG Server-Sent Events
- 관련 기술: MDN - Using server-sent events
- LLM 스트리밍 사례: OpenAI API Reference - Streaming events
- LLM 스트리밍 사례: Anthropic - Streaming Messages


Discussion
Comments
댓글은 승인 후 공개됩니다.