Server Sent Event | neruu00.log목록으로실시간 통신3 / 4
폴링부터 WebRTC까지 실시간 통신 방식을 하나씩 정리하는 시리즈의 세 번재 편입니다.
롱 폴링은 새로운 데이터가 생길 때까지 서버가 요청을 유지하기 때문에 폴링보다 빠르게 데이터를 전달 할 수 있었습니다. 하지만 한 가지 아쉬운 점이 있었습니다.
데이터를 하나 보내면 연결을 종료하고 다시 요청을 해야 한다는 점입니다.
지속적으로 서버에서 데이터를 받아야 한다면, 하나의 연결을 유지하며 서버가 필요할 때마다 데이터를 보내면 효율적일 것입니다.
이 방식이 SSE(Server Sent Event)입니다.
클라이언트의 SSE 코드는 굉장히 간단합니다.
const source = new EventSource("/api/events");
source.addEventListener("message", (event) => {
apply(JSON.parse(event.data));
});
브라우저에는 EventSource API가 기본적으로 제공되어 별도의 라이브러리가 없어도 됩니다.
롱 폴링처럼 응답을 받은 뒤 다시 요청을 보내는 로직이 필요하지 않습니다. 클라이언트는 연결을 열어두고 서버에서 이벤트가 오기를 기다립니다.
SSE의 핵심은 하나의 HTTP 연결을 유지하며 서버가 여러 이벤트를 순차적으로 전달하는 것입니다.
SSE는 일반적인 JSON 응답처럼 데이터를 한 번에 보내는 방식이 아닙니다.
서버는 text/event-stream 형식으로 이벤트를 전달합니다.
event: review-created
id : 1042
data: { "id": 1042, "title": "첫 번째 리뷰" }
event: review-deleted
id : 1043
data: { "id": 1043, "title": "두 번째 리뷰" }
data:에 실제 데이터를 담고, event:로 이벤트 종류를 구분합니다. 각 이벤트는 빈 줄로 구분합니다.
클라이언트에서는 이벤트 종류에 따라 처리할 수 있습니다.
source.addEventListener("review-created", (event) => {
});
source.addEventListener("review-deleted", (event) => {
});
형식이 이미 정해져 있기 때문에 애플리케이션에서 이벤트 구분을 위한 별도의 규칙을 처음부터 만들 필요가 없습니다.
SSE의 큰 장점 중 하나는 연결이 끊겼을 때 확인할 수 있습니다.
EventSource는 연결이 끊어지면 다시 재연결을 시도합니다. 서버는 retry 필드를 통해 재연결 간격을 지정할 수도 있습니다.
retry를 할 때 이벤트 ID를 함께 사용하면 재연결 이후 데이터를 이어서 받을 수 있습니다. 서버가 이벤트에 ID를 보내면 브라우저는 마지막으로 받은 이벤트 ID를 기억합니다. 다시 연결 할 때 이 값을 Last-Event-ID로 서버로 전달합니다.
GET /api/events
Last-Event-ID: 1043
서버는 이 값을 기준으로 1043번 이후에 발생한 이벤트를 다시 전달 할 수 있습니다.
연결이 끊기거나 요청이 바뀌는 사이에 데이터를 놓치지 않도록 마지막으로 어디까지 처리했는지 기억하는 것이 중요했습니다.
SSE에서는 이 부분을 브라우저가 어느 정도 표준화 되어 처리합니다. 하지만 서버가 모든 것을 알아서 처리해주는 것은 아닙니다.
서버가 이벤트마다 id를 부여해야 하고, 재연결한 클라이언트에게 이전 이벤트를 다시 전달할 수 있도록 필요한 데이터를 관리하고 있어야 합니다.
즉, 브라우저는 마지막 위치를 전달하고, 실제 데이터 복구는 서버가 담당합니다
SSE의 가장 큰 특징이자 한계는 통신 방향이 서버에서 클라이언트로 제한된 단방향 통신이라는 점입니다.
클라이언트에서 서버로 데이터를 보내야 한다면 SSE 연결을 사용할 수 없고 기존 HTTP API를 별도로 사용해야 합니다.
이런 구조는 실시간 상태 변경과 같은 서버에서 이벤트를 지속적으로 전달하지만, 클라이언트가 서버로 보내는 요청이 상대적으로 적은 로직에 적합합니다.
하지만 서버와 클라이언트가 모두 빈번하게 데이터를 주고받는 경우에는 적합하지 않습니다.
예를 들어 채팅을 구현한다면 메시지를 보낼 때는 HTTP 요청을 사용하고, 새로운 메시지를 받을 때는 SSE 연결을 사용해야 합니다. 기능 자체는 동작하지만 송수신을 서로 다른 방식으로 관리해야 한다는 복잡성이 생깁니다.
반면 WebSocket은 하나의 연결에서 양방향 통신을 지원하기 때문에, 클라이언트와 서버가 실시간으로 데이터를 주고받아야 하는 경우에 적합합니다.
다음 편에서는 양방향 통신을 위한 WebSocket이 어떻게 동작하는지 살펴보겠습니다.