🌍 네트워크

SSE

Server-Sent Events

서버에서 클라이언트로 단방향 실시간 데이터를 전송하는 웹 기술. HTTP 기반의 WebSocket 대안으로, EventSource API를 통해 실시간 알림, 피드, 주가 업데이트 등을 구현합니다.

📖 상세 설명

SSE(Server-Sent Events)는 서버가 클라이언트(브라우저)에게 단방향으로 실시간 데이터를 푸시할 수 있는 웹 표준 기술입니다. HTTP 프로토콜을 기반으로 작동하며, 서버에서 클라이언트로만 데이터가 흐르는 "단방향 통신"이 특징입니다. 2004년 Opera 브라우저에서 처음 구현되었고, 이후 HTML5 표준에 포함되었습니다.

SSE는 EventSource API를 통해 브라우저에서 쉽게 사용할 수 있습니다. WebSocket과 달리 별도의 프로토콜 핸드셰이크 없이 일반 HTTP로 작동하므로, 기존 인프라(로드밸런서, 프록시, 방화벽)와 완벽하게 호환됩니다. 연결이 끊어져도 브라우저가 자동으로 재연결을 시도하며, 마지막 이벤트 ID를 통해 놓친 데이터를 복구할 수 있습니다.

SSE의 메시지 형식은 매우 단순합니다: "data:", "event:", "id:", "retry:" 필드로 구성된 텍스트 스트림입니다. Content-Type은 "text/event-stream"을 사용합니다. 이 단순함 덕분에 서버 구현이 쉽고, 어떤 프로그래밍 언어로도 손쉽게 SSE 서버를 만들 수 있습니다.

실제 서비스에서 SSE는 실시간 알림(Facebook, Twitter), 주가/환율 업데이트, 스포츠 경기 실황, ChatGPT와 같은 AI 응답 스트리밍, CI/CD 빌드 로그, 뉴스 피드 등에 널리 사용됩니다. WebSocket이 양방향 통신에 적합하다면, SSE는 서버 푸시만 필요한 대부분의 실시간 시나리오에서 더 간단하고 효율적인 선택입니다.

⚖️ SSE vs WebSocket 비교

항목 SSE WebSocket
통신 방향 단방향 (서버 → 클라이언트) 양방향 (서버 ↔ 클라이언트)
프로토콜 HTTP/HTTPS WS/WSS (독자 프로토콜)
자동 재연결 브라우저 내장 지원 직접 구현 필요
데이터 형식 텍스트만 (UTF-8) 텍스트 + 바이너리
인프라 호환성 높음 (HTTP 기반) 별도 설정 필요할 수 있음
적합한 용도 알림, 피드, AI 스트리밍 채팅, 게임, 실시간 협업

💻 코드 예제

// SSE 클라이언트 - EventSource API
const eventSource = new EventSource('/api/events');

// 기본 메시지 수신
eventSource.onmessage = (event) => {
    console.log('데이터 수신:', event.data);
    const data = JSON.parse(event.data);
    updateUI(data);
};

// 연결 성공
eventSource.onopen = () => {
    console.log('SSE 연결 성공!');
};

// 에러 처리 (자동 재연결됨)
eventSource.onerror = (error) => {
    console.log('SSE 에러, 재연결 시도 중...');
    if (eventSource.readyState === EventSource.CLOSED) {
        console.log('연결 종료됨');
    }
};

// 커스텀 이벤트 타입 수신
eventSource.addEventListener('notification', (event) => {
    const notification = JSON.parse(event.data);
    showNotification(notification.title, notification.body);
});

eventSource.addEventListener('price-update', (event) => {
    const price = JSON.parse(event.data);
    updateStockPrice(price.symbol, price.value);
});

// 연결 종료 (컴포넌트 언마운트 시)
function cleanup() {
    eventSource.close();
    console.log('SSE 연결 종료');
}

// React/Vue에서 사용 예시
// useEffect(() => {
//     const es = new EventSource('/api/events');
//     es.onmessage = handleMessage;
//     return () => es.close();  // cleanup
// }, []);
// Express.js SSE 서버
const express = require('express');
const app = express();

app.get('/api/events', (req, res) => {
    // SSE 헤더 설정
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('Access-Control-Allow-Origin', '*');

    // 연결 유지를 위한 heartbeat
    const heartbeat = setInterval(() => {
        res.write(':heartbeat\n\n');
    }, 30000);

    // 클라이언트 ID 추적
    const clientId = Date.now();
    console.log(`클라이언트 연결: ${clientId}`);

    // 데이터 전송 함수
    function sendEvent(data, eventType = null, id = null) {
        if (id) res.write(`id: ${id}\n`);
        if (eventType) res.write(`event: ${eventType}\n`);
        res.write(`data: ${JSON.stringify(data)}\n\n`);
    }

    // 초기 데이터 전송
    sendEvent({ message: '연결 성공!', clientId }, 'connected');

    // 예시: 1초마다 시간 전송
    const interval = setInterval(() => {
        sendEvent({
            time: new Date().toISOString(),
            random: Math.random()
        });
    }, 1000);

    // 연결 종료 처리
    req.on('close', () => {
        clearInterval(interval);
        clearInterval(heartbeat);
        console.log(`클라이언트 연결 해제: ${clientId}`);
    });
});

// 여러 클라이언트에게 브로드캐스트
const clients = new Set();

app.get('/api/stream', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');

    clients.add(res);
    req.on('close', () => clients.delete(res));
});

function broadcast(data) {
    clients.forEach(client => {
        client.write(`data: ${JSON.stringify(data)}\n\n`);
    });
}

app.listen(3000);
# FastAPI SSE 서버
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse
import asyncio
import json

app = FastAPI()

# 간단한 SSE 엔드포인트
@app.get("/api/events")
async def events():
    async def event_generator():
        while True:
            # 데이터 생성
            data = {
                "timestamp": str(asyncio.get_event_loop().time()),
                "message": "Hello SSE!"
            }
            yield f"data: {json.dumps(data)}\n\n"
            await asyncio.sleep(1)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )

# sse-starlette 라이브러리 사용 (권장)
# pip install sse-starlette

@app.get("/api/notifications")
async def notifications(request: Request):
    async def event_stream():
        message_id = 0
        while True:
            if await request.is_disconnected():
                break

            message_id += 1
            yield {
                "event": "notification",
                "id": str(message_id),
                "retry": 5000,  # 재연결 대기 시간 (ms)
                "data": json.dumps({
                    "title": "새 알림",
                    "body": f"메시지 #{message_id}"
                })
            }
            await asyncio.sleep(2)

    return EventSourceResponse(event_stream())

# ChatGPT 스타일 AI 스트리밍 응답
@app.get("/api/chat")
async def chat_stream(prompt: str):
    async def generate_response():
        # AI 모델 응답 시뮬레이션
        response = "안녕하세요! SSE를 사용한 스트리밍 응답입니다."
        for char in response:
            yield f"data: {json.dumps({'token': char})}\n\n"
            await asyncio.sleep(0.05)  # 타이핑 효과
        yield f"data: {json.dumps({'done': True})}\n\n"

    return StreamingResponse(
        generate_response(),
        media_type="text/event-stream"
    )

🗣️ 실무에서 이렇게 말하세요

💬 기술 선택 회의에서
"실시간 알림 기능은 SSE로 구현하면 될 것 같습니다. 서버에서 클라이언트로만 푸시하면 되니까 WebSocket까지 쓸 필요는 없고요. HTTP 기반이라 로드밸런서 설정도 건드릴 필요 없어요. EventSource API가 자동 재연결도 해주니까 클라이언트 코드도 간단합니다."
💬 면접에서
"SSE는 HTTP 기반의 단방향 실시간 통신 기술입니다. WebSocket과 비교하면, SSE는 서버 → 클라이언트 방향만 지원하지만 구현이 단순하고 인프라 호환성이 좋습니다. ChatGPT가 답변을 스트리밍하는 것도 SSE를 사용하고요. 브라우저가 자동 재연결을 지원해서 네트워크 불안정 환경에서도 안정적입니다."
💬 코드 리뷰에서
"SSE 연결 시 heartbeat를 30초마다 보내는 게 좋을 것 같아요. 프록시나 로드밸런서가 유휴 연결을 끊을 수 있거든요. 그리고 event ID를 설정해두면 재연결 시 Last-Event-ID 헤더로 놓친 이벤트를 복구할 수 있습니다."

⚠️ 흔한 실수 & 주의사항

브라우저 연결 제한 무시

HTTP/1.1에서 브라우저는 도메인당 6개 연결만 허용합니다. SSE 탭을 여러 개 열면 다른 요청이 블로킹될 수 있습니다. HTTP/2를 사용하거나 연결을 공유하세요.

Heartbeat 없이 연결 유지

프록시, 로드밸런서, 방화벽이 유휴 연결을 30~60초 후 끊을 수 있습니다. 15~30초마다 코멘트(:heartbeat)나 빈 이벤트를 전송하세요.

버퍼링 문제 무시

Nginx, Apache 등이 응답을 버퍼링하면 SSE가 작동하지 않습니다. X-Accel-Buffering: no 헤더를 추가하거나 proxy_buffering off 설정이 필요합니다.

올바른 SSE 구현 패턴

retry 필드로 재연결 간격 설정, id 필드로 이벤트 추적, 연결 해제 시 리소스 정리, 적절한 에러 처리와 재연결 로직을 구현하세요.

🔗 관련 용어

📚 더 배우기