웹에서 서버가 클라이언트에게 실시간으로 데이터를 밀어 보내야 할 때가 있습니다. 알림, 진행 상황, LLM 토큰 스트리밍, 라이브 피드 같은 경우죠. Server-Sent Events(SSE)는 이를 위한 표준으로, 하나의 HTTP 응답을 열어 둔 채 서버가 텍스트 이벤트를 계속 흘려보내는 단방향(서버→클라이언트) 스트림입니다. 별도 프로토콜 없이 평범한 HTTP 위에서 동작해 구현이 간단하고, 프록시·방화벽 친화적입니다.
text/event-stream 형식
SSE 응답은 Content-Type: text/event-stream을 갖고, 본문은 특정한 텍스트 형식을 따릅니다. 각 이벤트는 필드: 값 줄들의 묶음이고, 빈 줄 하나가 이벤트의 끝을 표시합니다. 연결은 닫히지 않고 계속 유지되며, 서버는 새 이벤트가 생길 때마다 추가로 써 보냅니다.
GET /stream HTTP/1.1
Host: example.com
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
event: message
data: {"user":"kim","text":"hello"}
id: 101
event: message
data: 여러 줄도
data: 가능합니다
id: 102
retry: 5000
필드: event, data, id, retry
SSE 스트림에 쓰이는 필드는 소수이며 각자 역할이 분명합니다.
data: 이벤트의 실제 페이로드. 여러data:줄을 쓰면 개행으로 이어 붙여 하나의 데이터로 전달됩니다.event: 이벤트의 이름(타입). 지정하면 클라이언트에서addEventListener('그이름', ...)으로 받고, 생략하면 기본message이벤트가 됩니다.id: 이벤트의 식별자. 브라우저는 마지막으로 받은id를 기억해 두었다가 재연결 시 서버에 알려, 끊긴 지점부터 이어받게 합니다.retry: 연결이 끊겼을 때 재연결까지 기다릴 시간(밀리초). 서버가 재연결 간격을 제어할 수 있습니다.- (콜론으로 시작하는 줄
: comment은 주석으로, 연결 유지용 하트비트에 흔히 씁니다.)
EventSource와 자동 재연결
브라우저 클라이언트는 EventSource API로 SSE를 소비합니다. EventSource의 가장 강력한 장점은 자동 재연결이 내장돼 있다는 점입니다. 연결이 끊기면 브라우저가 retry 간격만큼 기다렸다가 스스로 다시 연결하고, 이때 마지막으로 받은 이벤트 id를 Last-Event-ID 요청 헤더에 실어 보냅니다. 서버가 이 헤더를 읽어 그 뒤 이벤트부터 재개하면, 네트워크가 불안정해도 이벤트 유실 없이 끊김을 메울 수 있습니다.
const es = new EventSource('/stream');
es.addEventListener('message', (e) => {
console.log('received:', e.data, 'id:', e.lastEventId);
});
es.onerror = () => { /* 브라우저가 자동으로 재연결 시도 */ };
SSE vs WebSocket vs 롱폴링
실시간 통신 수단은 상황에 따라 골라야 합니다. 롱폴링(long-polling)은 클라이언트가 요청을 보내고 서버가 데이터가 생길 때까지 응답을 미뤘다가 내려주는 방식으로, 매번 요청을 새로 열어야 해 오버헤드가 크지만 가장 호환성이 넓습니다. SSE는 하나의 연결로 서버가 계속 밀어 보내는 단방향 스트림이라 서버 푸시가 주된 시나리오(알림·라이브 업데이트·토큰 스트리밍)에 이상적이고, 자동 재연결·이벤트 ID 같은 편의가 표준에 내장돼 있습니다. WebSocket은 양방향 지속 연결이라 채팅·게임·협업 편집처럼 클라이언트도 자주 보내는 실시간 상호작용에 적합하지만, HTTP를 벗어난 별도 프로토콜이라 인프라와 재연결 로직을 직접 챙겨야 합니다. 요약하면, 서버가 주로 밀어 보내면 SSE, 양쪽이 활발히 주고받으면 WebSocket입니다.
chunked 전송과 주의점
SSE는 응답이 언제 끝날지 미리 알 수 없으므로 Transfer-Encoding: chunked(HTTP/1.1)나 스트리밍 본문으로 조각조각 전송됩니다. 실무에서 흔한 함정 몇 가지: (1) 중간의 프록시·서버가 응답을 버퍼링하면 이벤트가 한꺼번에 몰려 나오므로 버퍼링을 꺼야 합니다(예: nginx X-Accel-Buffering: no). (2) 브라우저의 HTTP/1.1 도메인당 연결 수 제한(약 6개)에 SSE 연결이 카운트되므로, HTTP/2 이상을 쓰면 다중화로 이 제약이 완화됩니다. (3) 유휴 연결이 중간 장비에 의해 끊기지 않도록 주석 줄로 주기적 하트비트를 보내는 것이 안전합니다.