서버 전송 이벤트(SSE)란? 2026년 개발자를 위한 가이드

발행: (2026년 5월 24일 PM 08:04 GMT+9)
11 분 소요
원문: Dev.to

출처: Dev.to

TL;DR

Server‑Sent Events (SSE)는 서버 → 브라우저 방향의 단방향 스트리밍 프로토콜이며, 순수 HTTP 위에서 동작합니다. 브라우저는 new EventSource(url) 로 연결을 열고, 서버는 그 연결을 계속 열어 두면서 원하는 시점에 데이터를 푸시합니다: 한 줄씩 전송합니다. 여기까지입니다. WebSocket 핸드셰이크도 없고, ws://도 없으며, 업그레이드도 없습니다. 자동 재연결을 지원하고, HTTP를 이해하는 모든 프록시를 통과하며, 2012년 이후 모든 브라우저에 내장돼 있습니다.

양방향 통신이 필요하지 않고 서버 → 클라이언트 스트리밍만 필요하다면, 2026년 현재 SSE가 WebSocket보다 거의 모든 면에서 우수합니다: 코드가 적고, 인프라가 단순하며, 자동 재연결, 재시작을 위한 자동 이벤트 ID 등을 제공합니다. WebSocket을 선택해야 하는 유일한 경우는 양방향 채팅형 트래픽이나 바이너리 프레임이 필요할 때입니다.

지금 바로 스트리밍을 확인하고 싶나요? 무료 SSE 테스터를 열고, 원하는 SSE 엔드포인트를 붙여넣으면 이벤트가 실시간으로 도착하는 것을 볼 수 있습니다.

[ Browser ]  ──── GET /events ───►  [ Server ]
              ◄── HTTP 200, keep alive
              ◄── data: hello\n\n
              ◄── data: world\n\n
              ◄── data: ...

이는 절대로 닫히지 않는 장기 HTTP GET 요청이며, 응답 본문은 특정 텍스트 포맷을 따릅니다. MIME 타입은 text/event-stream 입니다. 두 개의 개행(\n\n)이 나오면 하나의 이벤트가 onmessage 로 전달됩니다. 브라우저가 프레이밍, 파싱, 재연결을 모두 처리하므로 eventSource.onmessage = … 만 작성하면 끝입니다.

서버 (Node.js — 라이브러리 필요 없음)

import http from 'node:http';

http.createServer((req, res) => {
  if (req.url !== '/events') {
    res.writeHead(404).end();
    return;
  }

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

  let n = 0;
  const interval = setInterval(() => {
    res.write(`data: tick ${++n}\n\n`);
  }, 1000);

  req.on('close', () => clearInterval(interval));
}).listen(3000);

클라이언트 (어디서든 — <script> 태그 포함)

const es = new EventSource('/events');
es.onmessage = (e) => console.log('got:', e.data);
es.onerror = () => console.warn('disconnected, browser will retry');

서버를 실행하고 브라우저 콘솔에 클라이언트 코드를 붙여넣으면 20줄도 안 되는 코드로 스트리밍이 동작합니다. 빌드 단계도, 라이브러리도, 업그레이드 협상도 필요 없습니다. 이것이 바로 SSE의 전체적인 매력 포인트입니다.

많은 팀이 “실시간”이라는 단어만 듣고 무조건 WebSocket을 선택한 뒤, 프록시 타임아웃, 스티키 세션, ALB 업그레이드 헤더 디버깅에 일주일을 허비합니다. 실제 사용 사례의 절반 이상은 서버 → 클라이언트 일방향이었습니다 — 피드, 알림 서랍, 빌드 로그, 실시간 카운터 등. 바로 SSE가 담당할 영역이죠.

SSE를 사용하면 자동으로 얻는 기능 (WebSocket에서는 직접 구현해야 함)

  1. 자동 재연결
    연결이 끊어지면 브라우저가 약 3초 후에 자동으로 재연결합니다. 코드를 작성할 필요가 없습니다. WebSocket에서는 매 프로젝트마다 같은 백오프 로직을 직접 구현해야 합니다.

  2. 이벤트 ID와 재시작
    서버가 id: 42\n 를 보내면, 브라우저는 다음 재연결 시 Last-Event-ID: 42 헤더를 포함합니다. 서버는 중단된 지점부터 다시 전송할 수 있습니다. 빌드 로그, AI 스트리밍, 감사 피드에 최적입니다. WebSocket에서는 이를 직접 설계한 커스텀 프로토콜이 필요합니다.

  3. 표준 HTTP
    SSE는 GET 요청에 Accept: text/event-stream 만 있으면 됩니다. 모든 프록시, CDN, WAF, 리버스 프록시가 별다른 설정 없이 이해합니다(아래 주의사항 참고). 쿠키, Authorization, CORS, 압축 등도 그대로 동작합니다. WebSocket은 업그레이드 절차가 필요하고, 프록시가 이를 잘못 처리하는 경우가 많습니다.

  4. 프레이밍 없음
    텍스트만, 라인 구분만 하면 됩니다. 터미널에서 curl 로 SSE 엔드포인트를 직접 읽을 수 있습니다. WebSocket에서는 그렇게 할 수 없습니다.

curl -N -H "Accept: text/event-stream" https://api.example.com/events

SSE 포맷 (매우 단순하지만 실수하기 쉬움)

각 이벤트는 하나 이상의 라인으로 구성되며, 두 개의 개행(\n\n)으로 종료됩니다.

event: user-joined
id: 423
retry: 5000
data: {"userId":42,"name":"Ada"}

data: simple message
data: continuation of the same event
  • data: — 실제 페이로드. 같은 이벤트 안에 여러 data: 라인이 있으면 \n 으로 연결됩니다.
  • event: — 이벤트 이름. es.addEventListener('user-joined', …) 로 구독합니다. 생략하면 onmessage 로 전달됩니다.
  • id: — 재연결 시 브라우저가 Last-Event-ID 로 보내는 값.
  • retry: — 재연결 대기 시간(밀리초).
  • 빈 줄(\n\n) — 반드시 필요합니다. 두 번째 개행을 빼면 프록시가 이벤트를 버퍼링하고 클라이언트는 아무것도 받지 못하는 가장 흔한 SSE 버그가 발생합니다.

언제 SSE를 사용하고, 언제 WebSocket을 사용해야 할까?

SSE 사용 권장 상황

  • 트래픽이 서버 → 클라이언트 일방향일 때 (알림, 피드, 대시보드, 로그, AI 토큰 스트리밍)
  • 자동 재연결을 별도 구현하고 싶지 않을 때
  • 텍스트(JSON, markdown 조각, 로그 라인) 스트리밍일 때
  • curl 로 디버깅하고 싶을 때

WebSocket 사용 권장 상황

  • 트래픽이 양방향이며 고빈도일 때 (채팅, 협업 편집, 멀티플레이어 게임)
  • 바이너리 프레임이 필요할 때 (오디오/비디오, 커스텀 프로토콜)
  • 클라이언트 → 서버 메시지에 100ms 이하의 초저지연이 필요할 때

Long Polling 사용 권장 상황

  • 인프라가 SSE와 WebSocket을 모두 지원하지 못하는 경우 (2026년에도 기업 프록시가 가끔 방해함)

흔히 마주치는 SSE 버그와 해결책

  1. Nginx 버퍼링
    기본 설정에서는 응답을 버퍼에 저장합니다. res.write 가 버퍼에 머무르면 클라이언트는 몇 분 동안 아무것도 보지 못합니다. 두 가지 조치를 모두 적용하세요.

    • 서버 응답 헤더에 X-Accel-Buffering: no 추가

    • Nginx 설정에

      proxy_buffering off;
      proxy_cache off;
      proxy_read_timeout 24h;
  2. Cloudflare 버퍼링
    Cloudflare 역시 버퍼링합니다. SSE 엔드포인트를 캐시되지 않는 라우트에 두거나, Cloudflare‑aware 스트리밍 설정을 사용하세요.

  3. 브라우저 연결 제한
    동일 오리진당 HTTP/1.1 연결은 약 6개로 제한됩니다. 사용자가 여러 탭을 열면 7번째 탭은 연결 슬롯이 없어 대기합니다. 해결 방법:

    • HTTP/2 또는 HTTP/3 사용 → 하나의 TCP 연결에 수백 개 스트림을 멀티플렉싱
    • BroadcastChannel 혹은 SharedWorker 로 탭 간에 EventSource를 공유하고 이벤트를 브로드캐스트
  4. 로드 밸런서의 유휴 연결 종료
    AWS ALB는 기본 50초 후에 유휴 연결을 닫습니다. 15~30초마다 코멘트 라인을 전송해 연결을 유지하세요.

    setInterval(() => res.write(':keepalive\n\n'), 15000);

    : 로 시작하는 라인은 사양상 코멘트이며, 클라이언트 onmessage 를 트리거하지 않으면서 연결을 “따뜻하게” 유지합니다.

  5. EventSource 의 커스텀 헤더 제한
    EventSource 생성 시 커스텀 헤더를 지정할 수 없습니다. 따라서 Authorization: Bearer … 같은 헤더를 직접 넣을 수 없습니다. 선택지:

    1. 쿠키 (브라우저 앱에 권장) — 쿠키는 자동 전송됩니다. withCredentials: true 와 CORS 설정을 함께 사용하세요.

      const es = new EventSource('/events', { withCredentials: true });
    2. 쿼리 스트링 토큰 — 작동하지만 토큰이

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.