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 인코딩 같은 텍스트 인코딩으로 감싸야 한다.
  • 연결 복구: 브라우저의 EventSource API는 자동 재연결을 수행하고, 마지막으로 수신한 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

Source Trail