SSE는 단방향 연결로, 서버에서 클라이언트 측으로 이벤트를 보낸다
서버가 HTTP를 통해 클라이언트에 데이터를 전달할 수 있다. 또한 보통 HTTP 통신은 요청 1번에 응답 1번을 받는데, SSE는 한 번 연결하면 여러 번의 응답을 실시간으로 받을 수 있다.
WebSocket과 SSE는 실시간 통신이라는 공통점이 있다.
용도: WebSocket은 채팅, 게임, 실시간 협업 도구와 같이 빠른 양방향 통신이 필요한 곳에 적합하며, SSE는 뉴스 피드, 알림 등 서버에서 클라이언트로의 단방향 업데이트가 필요한 곳에 적합하다.
SSE는 Web API가 제공하는 EventSource를 통해 클라이언트에서 쉽게 연결할 수 있다.
EventSource는 서버로부터 이벤트 스트림을 수신하기 위한 표준 JavaScript 인터페이스로, 복잡한 구현 없이도 서버에서 보내는 실시간 알림을 받을 수 있게 해준다.
EventSource 인스턴스 생성하기이벤트 수신을 시작하려면, 이벤트를 생성하는 스크립트의 URL을 사용하여 새 EventSource 객체를 만든다.
const evtSource = new EventSource("sse-demo.php");
다른 도메인(요청 URL과 다른 URL)을 사용한다면 필요한 경우, withCredentials 속성을 true로 바꿔줘야 한다
const evtSource = new EventSource("//api.example.com/sse-demo.php", {
withCredentials: true,
});
🔮 withCredentials 속성
origin이 아닌 다른 도메인(URL) 간 쿠키, 인증 헤더를 사용해야 하는지 true / false로 여부를 나타내는 속성이다. 동일 출처 요청에는 영향을 미치지 않는다.
message 이벤트 수신하기서버에서 보낸 event 필드가 없는 메시지는 → message 이벤트로 수신된다.
메시지 이벤트를 수신하기 위해서는 message 이벤트를 위한 핸들러를 추가해야 한다.
evtSource.onmessage = function (e) {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = "message: " + e.data;
eventList.appendChild(newElement);
};
evtSource.onmessage 는 서버에서 보내는 기본 메시지 이벤트를 감지하는 핸들러이다. 서버에서 특별한 이벤트 이름 없이 데이터를 보내면 이 핸들러가 실행된다.
만약 특정 이벤트를 보낸다면, onmessage말고 addEventListener를 사용하면 된다
event 필드를 갖는 서버의 메시지들은 event 에 명시된 이름의 이벤트로 수신된다.
evtSource.addEventListener("ping", function (event) {
const newElement = document.createElement("li");
const time = JSON.parse(event.data).time;
newElement.textContent = "ping at " + time;
eventList.appendChild(newElement);
});
event 필드가 ping으로 설정된 메시지를 보낼 때마다 호출되며, data 필드의 JSON을 파싱하여 정보를 출력한다
🔮
data필드의 JSON을 파싱하는 이유
SSE에서 서버는 데이터를 항상 문자열로 보낸다. 객체나 배열등의 데이터 구조로 보내려면 서버 측에서 이를 JSON 문자열로 변환하여 전송한다. 따라서 클라이언트에서는JSON.parse()를 사용하여 문자열을 JavaScript 객체로 변환해주는 과정이 필요하다.
eventSource 인스턴스를 생성하여 서버가 보낸 이벤트를 수신할 수 있다.
event 이름이 없을 경우 onmessage 메서드로, event 이름이 있을 경우 addEventListner를 사용하여 이벤트를 수신할 수 있다.
/sse/subscribe 의 헤더에 Last Event ID를 넣어달라는 백엔드의 요청이 있었다.
로그인 후 구독만 하면 이벤트를 수신할 수 있는 것 아닌가? 라는 의문이 들었는데, 찾아보니 연결이 끊어지거나 오류가 나서 재요청할 경우 마지막에 발생한 event ID를 통해 재연결을 할 수 있도록 마지막 ID를 보내는 것이었다.
또한 이를 자동으로 관리해주는 event-source-polyfill이라는 라이브러리가 있어서 이것을 사용하기로 하였다. (기본 EventSource Web API만 사용해도 자동 재연결과 Last-Event-ID가 부분적으로 관리된다고 한다!)
IE 지원과 헤더 커스터마이징 측면에서도 라이브러리를 사용하여 구현하는 것이 더 좋을 것 같다.
const eventSource = new EventSourcePolyfill(
`${process.env.NEXT_PUBLIC_API_URL}${url}`,
{
...options,
}
);
eventSource.addEventListener('null', ((event: MessageEvent) => {
const eventData = event.data;
if (
eventData &&
typeof eventData === 'string' &&
eventData.startsWith('EventStream created')
) {
console.log('SSE connection established:', eventData);
setData({ connectionInfo: eventData } as unknown as T);
return;
}
}) as EventListener);
eventSource.addEventListener('ACHIEVEMENT', ((event: MessageEvent) => {
const eventData = event.data;
try {
console.log('data!!!!', event);
setData(JSON.parse(eventData));
setError(null);
} catch (parseError) {
console.error('SSE Data Parsing Error:', parseError);
setError(
parseError instanceof Error ? parseError.message : String(parseError)
);
setData(null);
}
}) as EventListener);
처음 subscribe를 성공하였을 경우에는 event 이름이 ‘null’로 들어오고, 이 후 이벤트를 수신했을 때는 ACHEIVEMENT, TIPS로 들어오기 때문에 로직을 분리해주었다.
테스트용 /sse/send API를 사용하여 이벤트가 제대로 수신되는지 확인해보았다
위에서 설명한 것처럼 데이터가 JSON 문자열로 전송되는 것을 볼 수 있다

데이터가 들어올 경우 파싱하여 객체로 변환한 후 콘솔에 찍히게 해보았다

객체 형태의 이벤트가 잘 들어오는 것을 볼 수 있다 !
이제 받아온 데이터를 원하는 형태로 보여주면 된다. 앱이 포커스된 상태에서는 토스트/스낵바로 보여주거나, 알림 센터/피드로 보여줄 수 있다.
또한 PWA의 기능을 활용하여 앱이 백그라운드에 있을 때도 서비스 워커를 통해 알림이 전송되게 할 수 있다.
참고한 글
XMLHttpRequest: withCredentials property - Web APIs | MDN
Using server-sent events - Web APIs | MDN
[NODE] 📚 Server Sent Events 💯 정리 (+사용법)
SSE(Server-Sent events) with EventSource