본문으로 건너뛰기
김신건의 로그

[Network] SSE (Server-Sent Events): 단방향 영속 스트림

· 수정 · 📖 약 2분 · 625자/단어 #sse #network #realtime #backend #streaming
SSE, Server-Sent Events, EventSource, text/event-stream

정의

Server-Sent Events (SSE)HTTP 위 단방향 (서버 → 클라이언트) 영속 스트림. text/event-stream MIME + 클라이언트의 EventSource API.

장점: HTTP/1.1 위에서 동작, 자동 재연결, 브라우저 native, 프록시/방화벽 친화.

메시지 포맷

data: hello\n
\n

data: line 1\n
data: line 2\n
\n

event: ping\n
data: {"t":1234}\n
\n

id: 42\n
data: msg\n
retry: 5000\n
\n
필드의미
data:본문 (여러 줄 가능, JOIN 으로 합쳐짐)
event:커스텀 이벤트 이름 (없으면 message)
id:메시지 ID. 재연결 시 Last-Event-ID 헤더로 보냄
retry:재연결 간격 (ms)
\n\n메시지 끝 구분

서버 응답

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {"price":42000}\n
\n

data: {"price":42100}\n
\n

클라이언트 (브라우저)

const es = new EventSource('/api/prices/stream');

es.onopen = () => console.log('open');

es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  console.log('price', data.price);
};

es.addEventListener('ping', (e) => {
  console.log('ping', e.data);
});

es.onerror = (e) => {
  // 자동 재연결 (브라우저가 처리)
  console.log('error', e);
};

// 명시 종료
es.close();

동작 흐름

sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: GET /stream<br/>Accept: text/event-stream
    S-->>C: 200 OK<br/>Content-Type: text/event-stream
    loop 연결 유지
        S-->>C: data: ...\n\n
    end
    Note over C: 네트워크 끊김
    Note over C: 자동 재시도 (3s 기본)
    C->>S: GET /stream<br/>Last-Event-ID: 42
    S-->>C: 200 OK (id=43 부터 재개)

TIP

서버는 Last-Event-ID 헤더를 검증하고 그 ID 이후 이벤트 만 보내야 완벽한 재개. 보통 이벤트 ID 를 DB 또는 Redis Stream offset 으로.

백엔드 구현 (Node)

app.get('/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  const lastId = req.headers['last-event-id'];
  const startFrom = lastId ? +lastId + 1 : 0;

  let id = startFrom;
  const t = setInterval(() => {
    res.write(`id: ${id}\n`);
    res.write(`data: ${JSON.stringify({ t: Date.now() })}\n\n`);
    id++;
  }, 1000);

  req.on('close', () => clearInterval(t));
});

SSE vs WebSocket vs Polling

항목SSEWebSocketLong Polling
방향서버 → 클라이언트양방향양방향
프로토콜HTTP/1.1 (또는 H2/H3)자체 (RFC 6455)HTTP
자동 재연결내장직접 구현직접
텍스트 / 바이너리텍스트만 (UTF-8)둘 다텍스트
프록시 / 방화벽 친화최고보통최고
브라우저 호환모던 OK모던 OK모두
서버 자원 (idle 연결)가벼움가벼움무거움 (반복 연결)

IMPORTANT

서버 → 클라이언트 일방향 푸시 (알림, 라이브 피드, 진행률) 면 SSE 가 거의 항상 더 단순. 양방향이 진짜 필요할 때만 WebSocket.

HTTP/2 위 SSE

HTTP/2 에서는 multiplexing 으로 여러 SSE stream + 일반 요청 한 연결에서. HTTP/1.1 의 6 connection limit 문제 해결.

활용

  1. 알림 / 토스트: 새 알림이 도착했을 때 push
  2. 라이브 대시보드: 메트릭 실시간
  3. AI Streaming (LLM 응답): OpenAI / Anthropic SDK 의 streaming 이 SSE 위에서
  4. 빌드 / 배포 로그 tail
  5. 주식 / 코인 시세 (단방향 충분)

흔한 함정

WARNING

  1. PROXY 의 response buffering = nginx 의 proxy_buffering on모든 데이터를 모아 보냄. SSE 는 즉시 flush 필요. proxy_buffering off + X-Accel-Buffering: no 헤더.
  2. HTTP/1.1 의 6 connection limit = 한 도메인에 6개 이상 SSE 불가. HTTP/2 로 해결.
  3. EventSource재연결 시 자동 인데 지수 백오프 없음 = 서버 과부하 위험. 서버에서 retry: 헤더로 조정.
  4. CORS preflight + EventSource = EventSource 는 기본 헤더만 지원. 커스텀 헤더 (Authorization) 가 필요하면 URL 쿼리 또는 쿠키.

관련 위키

이 글의 용어 (5개)
[Django] Channels: WebSocket, Consumer, Groupdjango
정의 Django Channels는 Django에 WebSocket, 비동기 프로토콜, 백그라운드 작업을 추가하는 공식 확장. 기본 Django는 HTTP만, Channels는 …
[Network] HTTP/1.1: keep-alive, pipelining, chunked transfernetwork
정의 HTTP/1.1 (1997, RFC 9112 으로 재정리) 는 텍스트 기반 stateless request/response 프로토콜. 2026 시점에도 대부분의 트래픽이 H…
[Network] HTTP/2: multiplexing, HPACK, server push의 종말network
정의 HTTP/2 (2015, RFC 9113) 는 HTTP/1.1 의 문법은 유지하되 전송을 바이너리 + 멀티플렉싱 으로 바꾼다. Head-of-Line Blocking (HT…
[Network] Realtime 전송 비교: Polling / SSE / WebSocket / WebRTCnetwork
정의 실시간 (서버 → 클라이언트) 데이터 전송 의 7가지 패턴 비교. 결정 트리 7가지 비교 | 패턴 | 방향 | 영속 | 프로토콜 | 사용 | |---|---|---|---|…
[Redis] Pub/Sub vs Streams: 휘발 신호 vs 영속 로그database-internals
정의 - Pub/Sub ( / ): 지금 듣고 있는 구독자 에게만 메시지가 전달되는 휘발성 신호. 영속 없음, ACK 없음. fan-out. - Streams ( / / / ):…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기