실시간으로 알림을 주는 서비스는 사용자의 요청이 없어도 실시간으로 서버의 변경 사항을 브라우저에 갱신해줘야 한다. 하지만 전통적인 client-server 모델의 http 통신에서는 이런 기능을 구현하기가 어렵다. 클라이언트의 요청이 있어야만 서버가 응답할 수 있기 때문이다.
http 기반으로 해당 문제를 해결하려면 다음과 같은 방식이 있다.
일정 주기를 가지고 서버의 API 를 호출하는 방법이다. 이 방식은 http 통신을 기반으로 하기 때문에 호환성이 좋다.
하지만 해당 방식은 업데이트 주기가 길다면 실시간으로 데이터가 갱신 되지 않고 짧다면 갱신 사항이 없음에도 서버에 요청이 들어와 불필요한 서버 부하가 발생한다는 단점이 있다.
Polling 과 비슷하나 업데이트 발생시에만 응답을 보내는 방식이다. 서버로 요청이 들어올 경우 일정 시간동안 대기했다가 요청한 데이터가 업데이트 되면 브라우저에게 응답을 보낸다. 불필요한 응답을 주는 경우를 줄이기 위해 사용할 수 있는 방법으로, 연결이 된 경우엔 실시간으로 데이터가 들어올 수 있다는 장점이 있다.
하지만 이 방식 또한 데이터 업데이트가 빈번하게 일어난다면 Polling 과 동일하게 서버에 부하가 발생할 수 있다.
웹 브라우저에서 서버쪽으로 특정 이벤트를 구독하면, 서버에서는 해당 이벤트 발생시 웹브라우저 쪽으로 이벤트를 보내주는 방식이다. 따라서 한 번만 연결 요청을 보내면, 연결이 종료될 때까지 재연결 과정 없이 서버에서 웹 브라우저로 데이터를 계속해서 보낼 수 있다.
다만, 서버에서 웹 브라우저로만 데이터 전송이 가능하고, 그 반대는 불가능하다는 단점이 있다. 또, 최대 동시 접속 횟수가 제한되어 있다.
서버와 웹브라우저 사이 양방향 통신이 가능한 방법이다. 변경 사항에 빠르게 반응해야하는 채팅이나, 리소스 상태에 대한 지속적 업데이트가 필요한 문서 동시 편집과 같은 서비스에 많이 사용되는 방식이다.
이 방식은 양방향 통신이 지속적으로 이루어질 수는 있으나, 연결을 유지하는 것 자체가 비용이 들기 때문에 트래픽 양이 많아진다면 서버에 큰 부담이 된다는 단점이 있다.
Polling 방식은 실시간성을 높이려면 그 주기를 짧게 해야 하는데, 트래픽이 많아질 경우 서버에 걸리는 부하가 커지기 때문에 알림 서비스에는 부적합하고 할 수 있다. Long-Polling 역시 마찬가지로, 트래픽이 많아지면 요청도 그만큼 많아지므로 부적합하다.
그렇다면 HTTP 연결 방식에 대한 부담이 적은 SSE와 WebSocket 방식이 남는데, 알림 서비스의 경우 클라이언트에서 서버로 데이터를 전송하지 않아도 되어서 단방향 통신만으로도 구현할 수 있으므로, SSE 방식으로 진행하기로 한다.
기존에 알림 서비스를 구현했던 방식은 Polling 방식이라서 사용자가 여럿 접속할 경우 부하가 커질 수 있었다.
그래서 SSE 방식으로 구현해보기로 하고 작업을 시작했다.
Content-Type: 브라우저에게 우리가 text 형태의 이벤트 스트림을 보낼 꺼다라고 말해주는 부분
Connection: 브라우저에게 커낵션을 닫지 말고 계속 열어두고 기다리고 있어라
계속 새로운 데이터가 올 것이기 때문에 캐시는 필요 없음으로 no-Cache
기억해야할 건 헤더에 아래처럼 보내줘야한다는 것과, 데이터를 보낼 때 맨 끝에 \n\n을 써줘야 하는 것. \n\n이 이벤트 스트림에 끝이라고 알려주는 방법이다.
...
res.writeHead(200, {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"Cache-Control": "no-cache",
});
setInterval(() => {
const data = result;
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
useEffect(() => {
const sseEvents = new EventSource('sse 를 처리할 end point')
sseEvents.onopen = function() {
// 연결 됐을 때
}
sseEvents.onerror = function (error) {
// 에러 났을 때
}
sseEvents.onmessage = function (stream) {
// 메세지 받았을 때
const parsedData = JSON.parse(stream.data)
}
}, [])
이렇게 클라이언트에서 EventSource로 연결을 하게 되면 네트워크 탭에서 아래와 같이 보이게 된다.