Server-Sent Events(SSE)는 서버에서 클라이언트로 단방향 실시간 데이터를 전송하는 기술입니다. WebSocket보다 훨씬 간단하고, HTTP 기반이라 기존 인프라와 호환성이 좋습니다. 알림, 실시간 피드, AI 스트리밍 응답에 적합합니다.

EventSource 기본 사용법

JS
// 서버에 연결
const eventSource = new EventSource("/api/events");

// 기본 메시지 수신
eventSource.addEventListener("message", (event) => {
  console.log("받음:", event.data);
});

// 연결 성공
eventSource.addEventListener("open", () => {
  console.log("SSE 연결됨");
});

// 에러 (네트워크 끊김 시 자동 재연결됨)
eventSource.addEventListener("error", (event) => {
  if (eventSource.readyState === EventSource.CONNECTING) {
    console.log("재연결 중...");
  } else {
    console.error("SSE 에러");
  }
});

// 연결 종료
eventSource.close();

서버 측 구현 (Node.js)

JS
// Express 예시
app.get("/api/events", (req, res) => {
  // SSE 헤더 설정
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  // 데이터 전송 형식: "data: 내용\n\n"
  const sendEvent = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // 주기적으로 데이터 전송
  const interval = setInterval(() => {
    sendEvent({ time: new Date().toISOString(), value: Math.random() });
  }, 1000);

  // 클라이언트 연결 종료 시 정리
  req.on("close", () => {
    clearInterval(interval);
  });
});

이벤트 타입 지정

서버에서 event: 필드를 사용하면 커스텀 이벤트를 보낼 수 있습니다.

JS
// 서버 측
res.write(`event: notification\ndata: {"message": "새 알림"}\n\n`);
res.write(`event: userUpdate\ndata: {"userId": 1, "status": "online"}\n\n`);

// 클라이언트 측 — 이벤트 타입별 리스너
const eventSource = new EventSource("/api/events");

eventSource.addEventListener("notification", (event) => {
  const data = JSON.parse(event.data);
  showNotification(data.message);
});

eventSource.addEventListener("userUpdate", (event) => {
  const data = JSON.parse(event.data);
  updateUserStatus(data.userId, data.status);
});

SSE 프로토콜 형식

PLAINTEXT
// 기본 메시지
data: 안녕하세요\n\n

// 여러 줄 데이터
data: 첫 번째 줄\n
data: 두 번째 줄\n\n

// 이벤트 타입 지정
event: chat\n
data: {"user": "정훈", "text": "안녕!"}\n\n

// ID 설정 (재연결 시 Last-Event-ID로 전송)
id: 12345\n
data: 메시지 내용\n\n

// 재연결 간격 설정 (밀리초)
retry: 5000\n
data: 재연결 간격 변경\n\n

자동 재연결과 Last-Event-ID

SSE의 가장 큰 장점 중 하나는 ** 자동 재연결 **입니다.

JS
// 서버 측 — ID 포함하여 전송
let messageId = 0;
const sendEvent = (data) => {
  messageId++;
  res.write(`id: ${messageId}\n`);
  res.write(`data: ${JSON.stringify(data)}\n\n`);
};

// 재연결 시 클라이언트가 보내는 헤더 확인
app.get("/api/events", (req, res) => {
  const lastEventId = req.headers["last-event-id"];

  if (lastEventId) {
    // 놓친 메시지부터 재전송
    const missedMessages = getMessagesSince(parseInt(lastEventId));
    missedMessages.forEach(sendEvent);
  }

  // ... 이후 실시간 메시지 전송
});

인증 처리 — EventSource의 한계

EventSource는 커스텀 헤더를 설정할 수 없습니다. 인증이 필요하면 대안이 필요합니다.

JS
// 방법 1: URL 쿼리 파라미터로 토큰 전달 (보안 주의)
const eventSource = new EventSource(`/api/events?token=${accessToken}`);

// 방법 2: 쿠키 기반 인증 (withCredentials)
const eventSource = new EventSource("/api/events", {
  withCredentials: true,
});

// 방법 3: fetch + ReadableStream으로 직접 구현
async function sseWithHeaders(url, headers) {
  const response = await fetch(url, { headers });
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // 이벤트 파싱
    const events = buffer.split("\n\n");
    buffer = events.pop(); // 마지막 미완성 이벤트는 버퍼에 유지

    for (const event of events) {
      if (event.trim()) {
        const data = event.replace(/^data: /gm, "");
        handleEvent(JSON.parse(data));
      }
    }
  }
}

sseWithHeaders("/api/events", {
  Authorization: `Bearer ${accessToken}`,
});

실전 활용 — AI 스트리밍 응답

JS
async function streamAIResponse(prompt) {
  const response = await fetch("/api/ai/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let fullText = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    fullText += chunk;
    updateChatUI(fullText); // 실시간 타이핑 효과
  }

  return fullText;
}

SSE vs WebSocket

기능SSEWebSocket
방향서버 → 클라이언트양방향
프로토콜HTTPws://
자동 재연결내장직접 구현
커스텀 헤더불가 (EventSource)불가 (핸드셰이크)
바이너리 데이터불가가능
브라우저 연결 제한도메인당 6개 (HTTP/1.1)제한 없음
구현 난이도쉬움중간
적합한 용도알림, 피드, AI 스트리밍채팅, 게임

** 기억하기 **: SSE는 서버에서 클라이언트로의 단방향 스트리밍이 필요할 때 가장 간단한 선택입니다. 자동 재연결이 내장되어 있고, HTTP 기반이라 기존 인프라와 호환됩니다. 양방향이 필요하면 WebSocket을, 단방향이면 SSE를 선택하면 됩니다.

댓글 로딩 중...