Server-Sent Events (SSE) 알람

프롤로그


Flask 에서 클라이언트에게 알람 메세지를 주기적으로 전달하기 위해서, Server-Sent Events (SSE) 를 통한 알람 메세지를 구현 하고자 했을 때 공부했던 내용과 구현 시 겪었던 시행착오와 경험을 적어봅니다.

소켓 vs SSE


소켓을 사용하지 않고 SSE 를 선택 이유는 간단합니다. 알람은 서버가 클라이언트에게 주기적으로 데이터를 전달하고 그중에 새로운 데이터가 있을 때만 특별히

새로운 알람이 왔어!

라고 알려주기만 하면, 클라이언트에서 받은 데이터로 리랜더링 하거나 새로운 알람을 받아오는 API 를 호출하면 되기에, 클라이언트와 양방향 통신이 필요치 않다고 생각했습니다. 몇가지 SSE 의 특징을 적어 보겠습니다.

  1. 텍스트 기반: 데이터는 순수한 텍스트 형식으로 전송됩니다.
  2. 단방향 통신: 데이터는 서버에서 클라이언트로만 전송됩니다. 클라이언트에서 서버로는 데이터를 보낼 수 없습니다.
  3. 지속적 연결: 한 번 연결이 수립되면, 서버는 연결을 지속적으로 유지하면서 데이터를 전송할 수 있습니다.
  4. 자동 재연결: 연결이 끊어졌을 경우 클라이언트는 자동으로 재연결을 시도합니다.

Flask SSE 구현


기본 적인 SSE 구현 Response 객체를 생성하여, content_typetext/event-stream 로 설정합니다. text/event-stream문자열 데이터를 반환하는SSE 의 콘텐츠 타입입니다. 서버가 클라이언트로 보내는 이 문자열 데이터는 특정한 규칙을 따릅니다. 우선은 기본적인 데이터를 보내는 형식입니다.

  1. data: 라는 문자열로 시작
  2. 보내고 싶은 문자열 데이터
  3. 각 메시지는 빈 줄(즉, \n\n)으로 끝나야 합니다.
main.py
from flask import Flask, Response
import time

app = Flask(__name__)

def generate():
    while True:
        # DO NOT forget the prefix and suffix
        yield f"data: Hello! {user_id}\n\n"
        time.sleep(5)

@app.get("/connection/<user_id>")
def connection(user_id: str):
    return Response(generator(user_id), content_type="text/event-stream")

if __name__ == "__main__":
    app.run(port=5000)
js
const eventSource = new EventSource("http://localhost:5000/connection/tiaz");

eventSource.onmessage = function(event) {
    console.log("New event:", event.data);
};

그림 1.

text/event-stream 필드


SSE 메시지는 일반 텍스트 형식으로 전송되며, 몇 가지 특별한 필드를 사용하여 구성됩니다. 주요 필드는 다음과 같습니다.

event 필드

원하는 이벤트의 이름을 지정합니다. 클라이언트는 이 이름을 사용하여 특정 이벤트 유형에 대한 리스너(listener)를 설정할 수 있습니다.

event: notice\n

data 필드

이벤트의 실제 데이터를 포함합니다. 데이터는 여러 줄에 걸쳐 있을 수 있으며, 각 줄은 data:로 시작해야 합니다. 여러 줄의 데이터는 연결되어 하나의 메시지로 처리됩니다. event 를 명시하지 않으면 message 라는 event 가 설정됩니다.

data: {"userId": 1, "status": "online"}
data: {"additional": "info"}

id 필드

이벤트 스트림의 마지막 이벤트 ID를 설정합니다. 연결이 끊어진 후 재연결 시, 클라이언트는 이 ID를 사용하여 끊어진 이후의 이벤트를 요청할 수 있습니다.

id: 12345\n

retry 필드

연결이 끊어진 경우 재연결을 시도하기 전 대기해야 하는 시간(밀리초 단위)을 지정합니다.

retry: 3000\n

이러한 필드들은 각각 개별적으로 사용될 수도 있고, 조합하여 사용될 수도 있습니다.

data: {"userId": 1, "status": "online"}

event: notice
data: {"userId": 2, "status": "offline"}

id: 67890
data: {"message": "Something happened"}

retry: 5000
data: {"retryMessage": "Please wait"}

이러한 형식을 따르면, SSE를 지원하는 웹 브라우저나 클라이언트는 이러한 메시지를 적절히 파싱하고, 필요한 동작을 수행할 수 있습니다.

서버에서 주의 사항


문자열 데이터를 반환하는SSE 는 위에서 설명한 형식을 따르면 됩니다. 다만, JSON 데이터를 보내고 싶은 경우, 보내고자 하는 data 필드에 원하는 데이터를 직렬화(Serialization) 를 수행해서 형식에 맞는 문자열로 구성해서 보내야 합니다.

yield f"""event: notice\ndata: {json.dumps(data)}\n\n"""
main.py
from flask import Flask, Response
from time import sleep
import json

app = Flask(__name__)

def generator(user_id):
    yield f"data: Hello! {user_id}\n\n"

    while True:
        data = {"name": user_id}
        yield f"""event: notice\ndata: {json.dumps(data)}\n\n"""
        sleep(5)

@app.get("/connection/<user_id>")
def connection(user_id: str):
    return Response(generator(user_id), content_type="text/event-stream")

if __name__ == "__main__":
    app.run(port=5000)
js
const eventSource = new EventSource("http://localhost:5000/connection/tiaz");

eventSource.onmessage = function(event) {
    console.log("New event:", event.data);
};

eventSource.addEventListener("notice", (e) => {
  console.log(event.data);
});

그림 2.

Server-Sent Events 정리


Server-Sent Events(SSE) 는 서버에서 클라이언트로 문자열 데이터를 전송 할 수 있는 단방향 통신 방법입니다. 구현 시 중요한 것은 규격에 알맞은 형태의 문자열을 구성하는 것 입니다. 각 필드의 규격을 정확히 확인하고 작성 하시길 바랍니다. 😊

실제 업무에서 구현하면서 만났던 문제는 🔗다음편 : Server-Sent Events (SSE) 알람 : 활용편 에서 적도록 하겠습니다.

flask sse