Server Sent Event (SSE)
Introduction
SSE(Server Sent Event)는 HTML5의 표준의며, 서버로부터 실시간으로 데이터를 받을 수 있는 읽기전용 API와 패턴을 정의합니다. 사용자에게 항상 최신의 정보를 유지해야하는 웹 페이지나 서버의 상태를 실시간으로 알 필요가 있는 환경에서 사용하기에 이상적입니다. SSE는 숏 폴링, 롤 폴링, HTTP 스트리밍을 모두 지원하며 서버와의 연결을 유지하기 위해 노력합니다. 만일 연결이 끊겼을 경우 자동으로 다시 연결을 시도합니다. 연결된 커넥션을 통해서 서버는 수시로 클라이언트에게 데이터를 송신할 수 있으며, 클라이언트는 SSE 명세를 통해 정의된 간단한 API를 통해서 데이터를 받아 사용할 수 있습니다. 결과적으로 Comet 같은 방식을 좀 더 사용하기 쉽게 해주는 API라고 할 수 있습니다. 그럼 잠깐, Comet은 무엇일까요?
Comet
Comet은 알렉스 러셀이 만든 용어로, 서버 푸시라고 부르기도 합니다. ajax는 클라이언트가 서버에게 요청을 하는 구조였다면, Comet은 서버가 클라이언트에게 데이터를 전달하는 방식입니다. Comet은 Long Polling과 Streaming 방식이 많이 사용됩니다.
Long Polling
Long Polling은 Short Polling과 비교해보는 것이 좋습니다.
Short Polling
client -> server : request
server -> client : response
rnote over server
update
endrnote
client -> server : request
server -> client : response with update
|||
client -> server : request
server -> client : response
Long Polling
client -> server : request
||40||
rnote over server
update
endrnote
server -> client : response with update
client -> server : request
||70||
server -> client : response
두 방식 모두 주기적으로 서버에서 HTTP 요청을 보내는 방식이란 점은 같습니다. 하지만, Short Polling은 서버에서 어떤 일이 있던 없던 그 즉시 현재 상태에 대해서 응답합니다. 반면, Long Polling은 요청을 받은 후 응답을 미룹니다. 서버에서 해당 요청에 해당하는 반응이 있을때 비로소 클라이언트에게 응답을 합니다. 만일 ajax의 timeout으로 인해 연결이 강제로 끊겨도 다시 재요청을 하며 서버의 응답을 기다립니다.
Streaming
폴링은 주기적으로 지속적인 HTTP 요청을 수행하지만, Streaming은 오직 한번의 요청과 하나의 연결 만을 유지합니다. 브라우저가 서버에 요청을 보내면 서버는 연결을 통해 주기적으로 데이터를 보내되 연결은 유지합니다.
Comet은 서버로부터 주기적으로 데이터를 가져올 수 있는 기능을 제공합니다. 그러나 Comet 방식을 사용하면 연결을 관리하는 일은 쉬운 일이 아닙니다. 그러던 중 마침 HTML5에 손쉬운 API를 제공하는 두 개의 표준이 등장하였습니다. 하나는 양방향 통신을 지원하는 WebSocket이며, 나머지 하나가 바로 서버 -> 클라이언트 단방향 통신을 지원하는 SSE(Server Sent Event) 입니다.
SSE 동작
SSE는 Long Polling 방식 또는 Streaming 방식으로 사용할 수 있습니다. WebSocket과는 달리 단방향 통신을 수행하며 별도의 프로토콜을 사용하지 않고 HTTP를 사용합니다. 대신 HTTP의 body를 SSE의 위한 특정 포맷으로 작성해야합니다. 그리고 HTTP Content-Type을 application/event-stream
으로 지정해야 합니다. 어떻게 보면 HTTP 위에 하나의 레이어를 하나 더 만든 격이죠.
SSE Message
SSE 메시지 형식은 간단합니다. 간단히 예제부터 보겠습니다.
data: This is first message
event: add
data: {"id": 1, "title": "hello", "author": "Seo"}
event: remove
data: 321321
첫 번째 줄의 data라는 키를 통해서 서버에서 클라이언트에게 전달하고자 하는 데이터를 입력하면 됩니다. 데이터의 타입은 plain/text
입니다. 각 메시지는 개행문자(\n)로 구분됩니다. 첫 번째 메시지는 data만 입력하였는데, 이 경우 기본 이벤트는 message입니다(message 이외에도 기본 이벤트가 몇가지 존재합니다).
두 번째 메시지에서는 event라는 항목을 확인할 수 있습니다. SSE는 서버로부터 이벤트를 트리거 받을 수 있습니다. 서버는 event 항목을 이용하여 트리거할 이벤트명을 지정할 수 있으며, 클라이언트는 받은 메시지를 확인하여 해당 이벤트를 트리거합니다. 위 예제의 경우 두 번째 메시지는 add 이벤트를 트리거하며, 세 번째 메시지는 remove 이벤트를 트리거합니다.
여기에서 주의해야할 점이 하나 있는데, 두번 메시지에서 전달하고 있는 data는 마치 JSON처럼 보이지만, 어디까지나 plain/text
타입입니다. 그렇기 때문에 위 문자열을 클라이언트는 받아서 파싱해서 사용할 필요가 있습니다.
주로 사용되는 event와 data 외에도 두가지 속성이 더 존재합니다.
- id: 메시지에 식별자를 부여
- retry: 연결이 끊겼을때 다시 재연결을 시도하기까지의 시간(reconnection time)
Default Events
SSE는 기본적으로 정의된 3가지 이벤트를 제공합니다.
- message
- 기본 메시지 이벤트. 서버로부터의 메시지에 event 속성이 존재하지 않는다면 기본으로 message 이벤트를 트리거합니다.
- open
- 연결이 되었을때 트리거됩니다.
- error
- 연결에 문제가 발생했을때 트리거됩니다.
EventSource
HTML5 SSE 표준에서는 이러한 방법들을 편리하게 사용할 수 있는 EventSource JavaScript 클래스를 정의합니다. EventSource는 사용법이 매우 간단하지만 강력한 기능을 가지고 있습니다.
var eventSource = new EventSource("/sse/cors/message");
var sse = new EventSource("/sse/message", { withCredentials: true });
sse.addEventListener('message', function (event) {
});
sse.addEventListener('add', function (event) {
// do something
});
sse.addEventListener('remove', function (event) {
// do something
});
EventSource의 사용법은 매우 간단합니다. JavaScript의 new 키워드를 통해서 EventSource를 생성하며 두 개의 파라미터를 받습니다. 첫 번째 파라미터는 URL입니다. EventSource 객체를 생성하자마다 전달한 URL로 즉시 비동기적으로 HTTP GET 요청을 시작합니다. 그리고 받은 메시지 해당하는 이벤트를 트리거합니다.
EventSource 객체에 이벤트를 등록하는 방법은 DOM의 이벤트 등록 방식과 동일합니다. 위 예제에서는 addEventListener를 사용하였지만, 아래와 같은 방법으로도 얼마든지 등록 가능합니다.
sse.onmessage = function (event) {
// do something
};
$(sse).on('message', function (event) {
// do something
});
만일 ES6에 추가된 Arrow function 문법을 사용한다면 더 깔끔해질 것 입니다.
sse.addEventListener('message', event => {
// do something
});
두 번째 파라미터는 옵션입니다. EventSource는 기본적으로 CORS 모드로 실행되는데, 만일 CORS 모드로 실행되는 것을 원하지 않는다면 withCredentials을 true로 설정하면 됩니다.
Server Example with Spring Web
SSE를 사용하기 위해서는 앞에서본 메시지 포맷에 맞추어서 응답해야합니다. 그래서 서버에서 특별한 작업을 추가해주여야 합니다. 하지만 SSE는 정식으로 등록된 표준인 만큼, 대부분의 웹 프레임워크들이 SSE에 관한 기능을 제공하고 있습니다. 여기에서는 가장 많이 사용되는 웹 프레임워크 중 하나인 Spring Framework를 기준으로 알아보겠습니다.
Spring Web은 SSE를 위한 SseEmitter라는 클래스를 제공합니다. SSE의 가장 목적이 간편함인 만큼 SseEmitter 클래스의 사용법도 정말 간단합니다.
@RequestMapping("/api/sse/somejob")
public SseEmitter doSse() {
SseEmitter sseEmitter = new SseEmitter();
CompletableFuture.runAsync(() -> {
// do something
sse.send("hello!!!");
// do something
sse.send(SseEmitter.event()
.name('add')
.data(someData, MediaType.APPLICATION_JSON));
// do something
};
return sseEmitter;
}
// somewhere
sseEmitter.send('bye');
sseEmitter.complete();