Conceptly
← 전체 목록
📡

Server-Sent Events

APIHTTP 응답을 열어 둔 채 서버가 이벤트를 흘려보내는 단방향 실시간 스트림

Server-Sent Events는 브라우저가 EventSource로 하나의 HTTP 연결을 열어 두고, 서버가 그 응답 안으로 이벤트를 순차적으로 밀어 넣는 단방향 실시간 전송 방식입니다. text/event-stream 포맷을 쓰며, 연결이 닫히지 않는 동안 서버는 data, event, id, retry 같은 필드를 계속 흘려보낼 수 있습니다. 실시간 알림이나 진행률처럼 서버에서 클라이언트로만 정보가 흐르면, 요청을 반복하지 않고도 화면을 즉시 갱신할 수 있게 해 줍니다.

아키텍처 다이어그램

🔄 프로세스 다이어그램

점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다

왜 필요한가요?

브라우저가 서버의 변화를 빨리 알아야 하는 화면에서는 요청-응답만으로는 비효율이 큽니다. 몇 초마다 상태를 확인하려고 같은 요청을 반복하면 아무 변화가 없어도 트래픽과 서버 작업이 계속 생깁니다. 반대로 확인 주기를 길게 잡으면 사용자는 이미 끝난 작업이나 새 알림을 한참 뒤에 보게 됩니다. 필요한 것은 브라우저가 직접 계속 물어보지 않아도, 서버가 변화를 생길 때마다 밀어 넣는 통로입니다. 특히 알림, 진행률, 라이브 피드처럼 서버에서 클라이언트로만 흐르는 정보는 이런 통로가 없으면 반응성이 금세 떨어집니다.

왜 이런 방식이 등장했나요?

초기 웹은 문서를 요청하고 응답받는 구조였기 때문에 짧은 요청-응답만으로도 충분했습니다. 하지만 알림, 대시보드, 작업 진행률처럼 변화가 생길 때마다 바로 알려 줘야 하는 화면이 늘면서, 같은 HTTP를 유지한 채 서버가 데이터를 조금씩 밀어 넣는 방식이 필요해졌습니다. 롱 폴링은 이 문제를 임시로 우회했지만, 연결을 계속 새로 만들고 끊는 비용이 남았습니다. Server-Sent Events는 EventSource와 text/event-stream 형식을 표준화해, HTTP 위에서 단방향 실시간 스트림을 안정적으로 제공하는 방법으로 등장했습니다.

안에서 어떻게 동작하나요?

브라우저가 EventSource로 URL을 열면 서버는 일반 응답을 끝내지 않고 헤더에 Content-Type: text/event-stream을 붙인 뒤 연결을 유지합니다. 이후 서버는 각 이벤트를 data 줄과 빈 줄로 나눠 흘려보내고, 브라우저는 이를 message나 custom event로 디스패치합니다. 연결은 한 번 열렸지만 끝나지 않기 때문에, 브라우저는 끊김이 생기면 자동으로 다시 연결하려고 시도합니다. 서버가 id를 보냈다면 마지막으로 받은 이벤트 이후 지점을 이어 받을 수 있고, retry로 재연결 간격도 제어할 수 있습니다. 중요한 제약은 이 채널이 서버에서 클라이언트로만 흐른다는 점입니다. 같은 연결 위에서 브라우저가 명령을 보내는 일은 할 수 없고, 클라이언트의 별도 요청이 필요합니다.

코드로 보면

서버가 스트림을 유지하며 이벤트를 흘려보내기

export function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue(encoder.encode("retry: 3000\n"));
      controller.enqueue(encoder.encode("event: progress\n"));
      controller.enqueue(encoder.encode("data: {\"step\":1}\n\n"));
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

`text/event-stream` 응답은 끝내지 않고 유지합니다. `data:` 줄과 빈 줄의 조합이 하나의 이벤트가 됩니다.

브라우저는 EventSource로 이벤트를 받아 처리한다

const source = new EventSource("/events");

source.addEventListener("progress", (event) => {
  const data = JSON.parse((event as MessageEvent).data);
  console.log(data.step);
});

source.onerror = () => source.close();

클라이언트는 직접 폴링하지 않습니다. 열린 연결에서 들어오는 이벤트를 EventSource가 자동으로 받아 디스패치합니다.

무엇과 헷갈리나요?

SSE와 WebSocket은 둘 다 연결을 오래 유지하지만, SSE는 서버에서 클라이언트로만 흘러가고 WebSocket은 양방향 메시지를 주고받습니다. 같은 실시간성이라도 쓰기 방향이 다릅니다. SSE와 REST도 자주 비교됩니다. REST는 요청마다 응답을 끝내는 구조이고, SSE는 하나의 응답을 열어 둔 채 이벤트를 계속 보냅니다. 실시간 변화가 연속적으로 들어오는지가 기준입니다.

언제 쓰나요?

SSE는 서버가 상태 변화를 계속 흘려보내야 하는 화면에 잘 맞습니다. 주문 처리 페이지에서 진행 단계가 바뀌는 순간, 관제 대시보드에서 경보가 발생하는 순간, 운영 로그 뷰어에서 새 항목이 도착하는 순간을 즉시 반영할 수 있습니다. 실무에서 중요한 것은 응답 버퍼링과 연결 유지입니다. 프록시나 CDN이 응답을 버퍼링하면 이벤트가 묶여서 늦게 도착할 수 있고, 서버는 끊어진 연결을 정리하면서 마지막 이벤트 ID를 이용해 이어 받기 흐름을 맞춰야 합니다. 브라우저에서 서버로 보내는 명령이 따로 필요하면 SSE만으로는 부족합니다. 이 경우에는 스트림은 수신용으로 두고, 쓰기 작업은 별도의 HTTP 요청으로 분리하는 구성이 자연스럽습니다.

실시간 알림진행률 표시라이브 피드대시보드 업데이트