에듀테크 프로젝트 대시보드
Server-Sent Events (SSE)
웹소켓처럼 연결을 열어두지만, 말은 서버만 한다 — 단방향이라 더 단순하고, 끊기면 브라우저가 알아서 다시 잇는다
주가 시세, 배포 진행 로그, 알림처럼 서버 → 클라이언트 한 방향으로만 데이터가 계속 흘러가면 되는 경우가 많다.
이럴 때 굳이 양방향 연결인 웹소켓을 쓰지 않고, 평범한 HTTP 응답을 끝나지 않는 스트림으로 열어두는 것이
SSE다. 브라우저 내장 EventSource API 하나로 연결·파싱·재연결까지 다 처리된다.
추가 프로토콜 업그레이드가 없어 방화벽·프록시 호환성이 좋고, 연결이 끊기면 클라이언트가 자동으로 재연결하며 마지막으로 받은 이벤트 ID부터 이어받을 수 있다 — 이 두 가지가 웹소켓과 가장 다른 지점이다.
"연결"을 누르면 서버가 무작위 간격으로 이벤트를 push한다고 가정하고 스트림을 흉내낸다. "네트워크 끊기"를 눌러
연결을 강제로 끊어보면, retry 시간 뒤 자동 재연결하며 마지막 id 이후부터 이어받는 과정이 보인다.
응답의 Content-Type이 text/event-stream이면, 서버는 연결을
닫지 않고 아래 형식의 텍스트 블록을 계속 흘려보낸다. 빈 줄(\n\n)이 이벤트 하나의 끝을 의미한다.
HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive # 이벤트 1 id: 101 event: price_update data: {"symbol":"KOSPI","price":2650.3} # 이벤트 2 (여러 줄 data는 이어붙여짐) id: 102 event: alert data: {"level":"warn", data: "msg":"거래량 급증"} # 재연결 시 대기 시간(ms) 지정 retry: 3000
| 필드 | 의미 |
|---|---|
| id | 이벤트 식별자. 재연결 시 Last-Event-ID 헤더로 서버에 다시 전달됨 |
| event | 이벤트 이름. 클라이언트에서 addEventListener(name, ...)로 구분 수신 |
| data | 실제 내용(보통 JSON 문자열). 여러 줄이면 개행으로 합쳐짐 |
| retry | 연결이 끊겼을 때 재시도까지 기다릴 시간(ms) |
const es = new EventSource('/stream/notifications'); es.addEventListener('price_update', (e) => { const data = JSON.parse(e.data); // e.lastEventId 로 마지막 id 확인 가능 console.log(data.symbol, data.price); }); es.onerror = () => { // 연결이 끊기면 브라우저가 retry 시간 뒤 자동 재연결 // 이때 Last-Event-ID 헤더를 자동으로 붙여 보냄 };
EventSource가
끊기면 retry 값만큼 기다렸다가 스스로 재접속하고, 요청 헤더에
Last-Event-ID: 102 처럼 마지막 id를 실어 보낸다. 서버는 이 값을 보고 놓친 이벤트부터 다시 흘려주면 된다.
from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse import asyncio, json app = FastAPI() async def event_gen(last_id: int): eid = last_id while True: await asyncio.sleep(2) eid += 1 payload = json.dumps({"price": 2650 + eid}) yield f"id: {eid}\nevent: price_update\ndata: {payload}\n\n" @app.get("/stream/notifications") async def stream(request: Request): last_id = int(request.headers.get("last-event-id", 0)) return StreamingResponse(event_gen(last_id), media_type="text/event-stream")
| 상황 | 추천 | 이유 |
|---|---|---|
| 서버 → 클라이언트 단방향 스트림 | SSE | 구현 단순, HTTP 그대로, 자동 재연결·Last-Event-ID 내장 |
| 채팅처럼 양쪽 다 실시간 전송 | 웹소켓 | 양방향 저지연 통신이 필요할 때만 |
| 몇 분에 한 번 갱신이면 충분 | 폴링 | 연결 유지 자체가 필요 없는 낮은 빈도 |
단점도 있다: SSE는 텍스트 전용이라 바이너리를 못 보내고, 브라우저별로 한 origin당 동시 연결 수 제한(HTTP/1.1 기준 보통 6개)이 있다. 또 오직 서버→클라이언트 방향만 가능해, 클라이언트가 뭔가 보내려면 별도의 일반 HTTP 요청을 함께 써야 한다.
SSE는 "서버만 계속 말하는" 가벼운 실시간 채널이다. 평범한 HTTP 위에서 동작해 인프라 호환성이 좋고,
EventSource가 재연결·이어받기까지 알아서 처리해준다는 점이 웹소켓과의 핵심 차이다.