Server Side Events(SSE)

4 minute read

개요

실시간 데이터를 보여줘야하는 화면이 있다고 가정한다.
Web에서 많이 사용하는 HTTP request-response 모델은 클라이언트(브라우저)가 어떤 리소스에 대해 요청을 보내고, 그에 맞는 응답을 서버가 내려주는 형태다. 이런 모델은 정적인 컨텐츠를 보여줄 때에는 알맞지만, 실시간 데이터를 보여줄 때에는 적합하지 않다. 언제 실시간 데이터를 요청할지 프론트는 모르기 때문이다.
실시간 데이터 화면을 구성하려면, 백엔드가 실시간으로 업데이트된 데이터를 프론트엔드에 전달해줘야한다. 실시간 데이터를 전송할 때 사용하는 방법은 WebSocket과 Server Side Events(SSE)와 같이 여러가지가 있다. 전 회사에서 WebSocket은 사용해봤는데 SSE는 이번 업무를 진행하면서 처음으로 검토해 본 것이라, SSE에 대해 알아보고 기록한 내용을 공유한다.

Sever Side Events(SSE)란

  • SSE는 HTTP 커넥션을 통해 클라이언트가 서버로부터 자동적으로 데이터를 받게끔해주는 Push technology이다.
  • 서버와 클라이언트 사이의 HTTP 커넥션이 맺어지면, 서버는 자동적으로 새로운 데이터를 보낼 수 있다.
  • SSE는 HTTP 프로토콜을 사용하며, 클라이언트(브라우저)는 Web API EventSource를 사용해 event source(이벤트가 발생한 곳, 서버 api)에 등록해 줘야 한다.
  • automatic reconnection: 클라이언트가 이벤트 소스에 접속이 끊어지면, 자동적으로 재접속을 한다.
  • 보내진 각 event 별로 고유 event id를 가질 수 있다.

  • 요즘 사람들이 많이 사용하고 있는 ChatGPT 채팅도 SSE를 사용하고 있다고 한다.
    • 사용자가 질문을 보내면 ChatGPT서버가 답변 문장의 단어 하나하나 작성할 때 SSE를 사용해서 보내준다고 한다.
    • 사용자가 그 다음 질문을 보내면 기존 SSE 접속을 끊고, 새로운 SSE접속을 통해 응답을 받아오는 형태인 것 같다.

SSE vs WS(Web Socket)

SSE와 WS의 차이를 알아본다. 1

항목 WS SSE
데이터 흐름 방향 양방향 데이터 전달 (서버 <-> 클라이언트) 단방향 데이터 전달 (서버 -> 클라이언트)
브라우저 지원 여러 브라우저가 지원2 IE는 지원 안함3
data transmission forma(데이터 보내는 포맷) UTF-8 encoded 텍스트/바이너리 포맷 둘 다 지원 UTF-8 encoded 텍스트
프로토콜 WebSocket Protocol 사용 단순 HTTP 사용
사용 예제 양방향 채팅 앱, 멀티 플레이어 게임 상태 업데이트, 푸쉬 notification 앱

limitations 제한

  • SSE는 HTTP/2 버전에서 돌리지 않으면, 브라우저+도메인당 최대 6개 접속만 가능하다.4
    • 브라우저+도메인당이라는 말은, chrome브라우저에서 www.choi.com 이란 도메인이란 이름으로 최대 6개의 SSE 접속이 가능하고, www.choi2.com란 이름으로 최대 6개의 SSE 접속이 가능하다는 뜻이다.

코드 예제

  • SSE를 활용한 단순한 클라이언트(프론트), 서버(백) 코드를 설명한다.
  • 우선 챗지피티의 도움을 받아 express를 사용한 서버쪽 코드는 다음과 같다.
const express = require('express');
const app = express();
  
// Enable CORS for SSE
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

// SSE route
app.get('/sse', (req, res) => {
  // Set headers to allow SSE
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // Send initial SSE event
  res.write('event: message 초이 타입\n');
  res.write('data: Hello from SSE!\n\n');

  // Simulate sending SSE events at intervals
  const intervalId = setInterval(() => {
    res.write('event: message\n');
    res.write(`data: Server time: ${new Date().toLocaleString()}\n\n`);
  }, 1000);

  // Handle client disconnect
  res.on('close', () => {   // listen for close event on the response object  <- triggers when the client disconnects
    clearInterval(intervalId);
  });
});

// Handle other routes
app.get('/', (req, res) => {
  res.send('Hello, SSE!');
});

// Start the server
const port = 3000;
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});
  • mdn사이트5에 따르면 event를 보내는 서버는 MIME type text/event-stream을 따라야 한다고 한다.
  • event stream format에 관하여는 이 mdn 사이트 페이지6에서 찾을 수 있다.
    • 이벤트 스트림은 텍스트 데이터 스트림으로,UTF-8로 인코딩 되어 있어야 한다.
    • 각 이벤트 스트림 메세지는 필드 이름: 텍스트 데이터형식으로 이루어 진다.
    • 필드에는 event, data, id, retry가 있다.
    • event 필드: 이벤트 타입을 명시하는 스트링. 특정 이벤트 이름을 명시하면, 해당 이름을 가진 이벤트로 브라우저로 디스패치된다.
  • 예제 서버 코드에서는 "/sse"로 들어온 sse 리퀘스트로, setInterval을 사용하여 1초마다 현재 서버 타임 메세지를 보내도록 한다.

  • sse 요청을 보내고 서버한테 전달받은 메세지를 화면에 뿌리는 클라이언트쪽 코드는 다음과 같다:
// 메세지를 뿌리기위해 html message element 추가하는 코드
const msgElt = document.querySelector(".messages");

function addMessage(msg) {
  const div = document.createElement("div");
  div.className = "message";
  div.textContent = msg;
  msgElt.appendChild(div);
}

// SSE 수신 코드 시작
let eventSource; 

function sslConnect() {
  console.log(`ssl connect!`)
  eventSource = new EventSource('http://localhost:3000/sse');
  // Event listener for SSE messages

  // 연결되었을 때 호출
  eventSource.onopen = (event) => { //Event
    console.log(`OPEN!`, event);
    addMessage("OPEN!");
  };

  // 서버 message수신될 때 호출
  eventSource.onmessage = (event) => { // MessageEvent
  // eventSource.addEventListener('message', (event) => {
    // const eventData = JSON.parse(event.data);
    // console.log('Received SSE:', eventData);
    console.log('Received SSE:', eventSource, event.data, event);
    addMessage(event.data);
  };

  // Event listener for SSE errors
  eventSource.addEventListener('error', (error) => {
  // eventSource.onerror((error) => {
    console.warn('SSE error:', error);
  });
}

function sseDisconnect(){
  eventSource.close();
  console.log('Disconnected from SSE server', eventSource);
}
  • EventSource 인스턴스를 생성해 서버 주소를 넣어 서버 접속을 시작한다. messageerror이벤트를 수신한다.
  • 수신한 이벤트리스너에 메세지 이벤트가 도착시, 해당 메세지를 화면에 추가해서 보여준다.
  • sseConnect, sseDisconnect 함수는 각각 CONNECT,DISCONNECT버튼 클릭시 호출된다.
  • 예제 HTML은 다음과 같다:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

</head>
<body>
  <h1>Client Side</h1>
  SSE Test

  <div class="action-btns">
    <button onclick="sslConnect()">CONNECT</button>
    <button onclick="sseDisconnect()">DISCONNECT</button>
  </div>

  <div class="messages">
    <div class="message">sample msg</div>
  </div>
  <script src="main.js"></script>

</body>

<style>
  body {
    color: salmon;
  }
  .messages {
    overflow: scroll;
    border: 1px solid yellowgreen;
    height: 500px;
  }
  .message {
    color: darkorange;
  }
</style>
</html>
  • CONNECT, DISCONNECT, CONNECT 순서로 버튼을 눌렀을 때 보여지는 화면이다.

SSE 활용1

  • 두번째 CONNECT 버튼을 누를 때, 처음 SSE 커넥션을 지우고, 새로운 SSE 커넥션을 맺는 것을 네트워크 탭에서 볼 수 있다.

SSE 활용2

sidenote - 검토 결론

  • 탭당 6개 접속도 아니고, 브라우저당 6개라면, 실상, HTTP/2 버전 아니면 못쓴다고 판단했다.
  • 검토당시 회사 앱은 HTTP 1.1에서 돌아가고 있었고, SSE를 쓰기 위해 HTTP 버전 2로 올려야된다고 판단했다. HTTP 버전올리는걸 단순하게 생각했는데, devops쪽에서는 이게 번거로운 일이라고 건너들었다. 검색해보니, 현재 나와있는 라이브러리들도 HTTP2에서 완벽히 안돌아갈 수도 있다고 작성된 글도 읽었다. HTTP버전 업그레이드는 못한다고 생각해서 SSE는 드롭되고, WebSocket을 쓰기로 내부적으로 결정되었다.
  • WebSocket보다 구현하기 단순해서, 써보고 싶었는데 아쉽다.

References

Leave a comment