서버 전송 이벤트(Server-Sent Events, SSE)는 HTTP(HyperText Transfer Protocol) 응답 하나를 닫지 않고 열어 둔 채, 서버가 원할 때마다 텍스트 이벤트를 흘려보내는 서버 → 클라이언트 단방향 푸시 방식이다. HTML 표준에 정의돼 있고 브라우저는 EventSource로 받는다.
const source = new EventSource('/api/orders/stream')
source.onmessage = (e) => render(JSON.parse(e.data)) // 이름 없는 이벤트
source.addEventListener('status', (e) => update(e.data)) // event: status
source.onerror = () => { /* 브라우저가 알아서 다시 연결한다 */ }Content-Type: text/event-stream
event: status
id: 42
data: {"step":"shipping"}
: 주석 줄(연결 유지용 핑)
retry: 5000- 응답 타입은
text/event-stream, 인코딩은 항상 UTF-8. 필드는event(이벤트 이름),data(본문, 여러 줄이면 이어 붙임),id,retry(재연결 대기 ms)이고 빈 줄이 이벤트 하나의 끝이다 - 자동 재연결이 표준에 들어 있다. 끊기면 브라우저가 다시 연결하면서 마지막으로 받은
id를Last-Event-ID헤더로 보내 서버가 이어서 보낼 수 있다. 서버가204 No Content로 답하면 재연결을 멈춘다 EventSource는 URL과withCredentials만 받는다. 요청 헤더나 본문을 넣을 수 없어 인증은 쿠키로 하거나,fetch+ReadableStream으로 같은 형식을 직접 파싱한다- HTTP/1.1에서는 브라우저의 도메인당 동시 연결 제한(6개)에 걸려 탭을 여러 개 열면 막힐 수 있다. HTTP/2에서는 한 연결 안의 스트림으로 다뤄져 여유가 크다
- 응답이 캐시되지 않도록
Cache-Control: no-cache를 함께 보낸다
WebSocket과 비교
| SSE | WebSocket | |
|---|---|---|
| 방향 | 서버 → 클라이언트 | 양방향 |
| 프로토콜 | 평범한 HTTP 응답 | HTTP에서 업그레이드한 별도 프로토콜 |
| 데이터 | UTF-8 텍스트 | 텍스트·바이너리 |
| 재연결·이어받기 | 표준 내장 | 직접 구현 |
알림, 진행 상태, LLM(Large Language Model) 토큰 스트리밍처럼 받기만 하면 되는 경우는 SSE가 단순하다. 채팅·협업 편집처럼 클라이언트도 자주 보내야 하면 WebSocket을 쓴다. 이벤트를 받아 TanStack Query 캐시를 갱신하면 주기적 재요청(polling)을 대신할 수 있다.
RSC 스트리밍과 무엇이 다른가
둘 다 응답을 조금씩 흘려보내지만 언제, 무엇을 흘리느냐가 다르다.
- 스트리밍 SSR·RSC 페이로드는 한 번의 요청(첫 로드나 이동)으로 한 화면을 점진적으로 완성한다. 서버가 UI(HTML, 직렬화된 컴포넌트 트리)를 보내고, 화면이 다 채워지면 응답이 끝난다
- SSE는 화면이 자리 잡은 뒤에도 연결을 유지하며 서버가 자기 시점에 데이터를 보낸다. 받은 데이터로 UI를 그리는 일은 클라이언트 몫이다
그래서 "첫 화면을 구역별로 빨리 보여 주기"는 RSC를 쓰는 앱이라면 Suspense 경계로 이미 풀리고, "로드 이후 서버가 먼저 알려 주기"는 RSC가 대신하지 못해 SSE나 WebSocket이 필요하다.
출처: MDN - Using server-sent events · HTML Standard - Server-sent events