Server-Sent Events (SSE): 간단하지만 강력한 실시간 통신

Server-Sent Events

SSE란?

Server-Sent Events(SSE)는 HTTP를 통해 서버가 클라이언트에게 실시간으로 데이터를 푸시하는 표준 기술이다.

const eventSource = new EventSource("/api/stream");

// 연결 성공 시
eventSource.onopen = () => {
  console.log("✅ 연결 성공");
};

// 메시지 수신 시
eventSource.onmessage = (event) => {
  console.log("📨 새 데이터:", event.data);
};

// 에러 발생 시 (자동 재연결 시도)
eventSource.onerror = (error) => {
  console.error("❌ 연결 오류:", error);
};

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

주요 특징:

  • HTTP 기반의 단방향 통신 (서버 → 클라이언트)
  • 자동 재연결 지원
  • 기존 HTTP 인프라 활용

적합한 사용 사례:

  • 실시간 알림, 뉴스 피드
  • 주식 시세, 대시보드 업데이트
  • 진행 상태 모니터링
  • LLM 스트리밍 응답 (ChatGPT, Claude 등)

작동 원리

SSE는 HTTP/1.1의 Chunked Transfer Encoding을 활용한다. 서버는 응답을 종료하지 않고 계속해서 데이터를 전송한다:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked

data: 첫 번째 메시지\n\n
data: 두 번째 메시지\n\n
data: 세 번째 메시지\n\n
... (계속)

일반 HTTP는 0\r\n\r\n (종료 청크)를 보내며 끝나지만, SSE는 절대 종료하지 않는다. 이렇게 연결을 열어둔 채로 새 이벤트를 계속 전송한다.

이벤트 형식

event: userUpdate
data: {"userId": 123, "status": "online"}
id: 1

  • event: 이벤트 타입 (미지정시 'message')
  • data: 실제 데이터
  • id: 이벤트 ID (재연결 시 필요)
  • 메시지는 빈 줄(\n\n)로 구분

재연결 메커니즘

연결이 끊어지면 브라우저가 자동으로 재연결하며, Last-Event-ID 헤더로 마지막 이벤트 ID를 전송한다:

// 서버에서 확인
const lastEventId = req.headers["last-event-id"];
if (lastEventId) {
  // 누락된 이벤트만 재전송
  sendMissedEvents(lastEventId);
}

이를 통해 중간에 놓친 메시지를 자동으로 복구할 수 있다.

HTTP/2의 중요성

HTTP/1.1의 문제:

  • 브라우저당 도메인당 6개 연결 제한
  • SSE가 연결을 영구 점유 → 다른 요청 차단

HTTP/2의 해결책:

  • 단일 연결에서 100+ 스트림 동시 처리
  • Multiplexing으로 SSE와 일반 요청 공존
HTTP/1.1: 6개 연결 제한 → SSE 3개면 나머지 3개로 모든 요청 처리
HTTP/2:   하나의 연결 → SSE + API + 이미지 등 100+ 동시 처리

결론: 프로덕션에서는 HTTP/2 필수

WebSocket과의 차이

특징SSEWebSocket
통신 방향단방향양방향
재연결자동직접 구현
프로토콜HTTPWebSocket
복잡도낮음높음

언제 SSE를 사용할까?

✅ SSE가 적합한 경우:

  • 실시간 알림, 뉴스 피드
  • 대시보드, 모니터링
  • LLM 스트리밍 응답 (ChatGPT, Claude 등)
  • 진행 상태 추적
  • 서버 → 클라이언트 단방향 데이터 푸시

언제 WebSocket을 사용할까?

❌ WebSocket이 필요한 경우:

  • 양방향 통신 (채팅, 게임, 협업 도구)
  • 바이너리 데이터 전송
  • 매우 낮은 지연시간이 중요한 경우
  • 클라이언트 → 서버 실시간 전송 필요

보안 체크리스트

  • 인증: Cookie 기반 권장 (EventSource는 커스텀 헤더 미지원)
  • CSRF: Origin 헤더 검증 + SameSite 쿠키
  • XSS: textContent 사용 (innerHTML 금지)
  • HTTPS: 필수

직접 체험해보기

아래 데모에서 SSE 기반 AI 스트리밍을 실시간으로 체험할 수 있다. 질문을 입력하고 전송 버튼을 눌러보자.

참고: 이 데모는 실제 AI API를 호출하지 않으며, 미리 준비된 응답을 스트리밍한다. 실제 구현에서는 OpenAI, Anthropic 등의 API를 연동하면 된다.

AI 스트리밍 데모
질문을 입력하고 Enter를 눌러보자. 실시간으로 응답이 스트리밍된다.
⚪대기 중
메시지를 입력해서 대화를 시작해보자

실전 구현: AI 스트리밍 데모 분석

위 데모의 실제 구현을 단계별로 살펴보자.

1. 서버: Next.js API Route

/api/ai-stream에서 ReadableStream으로 문자 단위 스트리밍을 구현한다.

// app/api/ai-stream/route.ts
export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      // ① 연결 확인 메시지
      controller.enqueue(encoder.encode('data: {"status": "connected"}\n\n'));

      // ② Mock 데이터 문자 단위 스트리밍 (20-40ms 지연)
      for (const char of mockText) {
        const delay = 20 + Math.random() * 20;
        await new Promise((resolve) => setTimeout(resolve, delay));

        const chunk = JSON.stringify({ text: char, done: false });
        controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
      }

      // ③ 완료 신호
      controller.enqueue(encoder.encode('data: {"done": true}\n\n'));
      controller.close();
    },
  });

  // SSE 필수 헤더
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

핵심 포인트:

  • ReadableStream으로 문자를 하나씩 전송
  • text/event-stream 헤더로 SSE 프로토콜 준수
  • done: true로 스트리밍 종료 신호 전달

2. 클라이언트: useAiStream Hook

EventSource를 캡슐화하여 상태 관리를 제공한다.

// features/sse-demo/lib/use-ai-stream.ts
export function useAiStream() {
  const [streamedText, setStreamedText] = useState("");
  const [connectionState, setConnectionState] =
    useState<ConnectionState>("idle");
  const eventSourceRef = useRef<EventSource | null>(null);

  const startStream = useCallback(() => {
    const es = new EventSource("/api/ai-stream");
    eventSourceRef.current = es;

    es.onopen = () => {
      setConnectionState("connected");
    };

    es.onmessage = (event) => {
      const data = JSON.parse(event.data);

      if (data.status === "connected") return; // 연결 확인 무시
      if (data.done) {
        // 완료 처리
        es.close();
        setConnectionState("disconnected");
        return;
      }

      // 텍스트 누적
      if (data.text) {
        setStreamedText((prev) => prev + data.text);
      }
    };

    es.onerror = () => {
      setConnectionState("error");
      es.close();
    };
  }, []);

  // cleanup: 언마운트 시 연결 종료
  useEffect(() => {
    return () => {
      if (eventSourceRef.current) {
        eventSourceRef.current.close();
      }
    };
  }, []);

  return { streamedText, connectionState, startStream, stopStream };
}

핵심 포인트:

  • useRef로 EventSource 인스턴스 보관 (재렌더링 방지)
  • 연결 상태를 5단계로 추적 (idle → connecting → connected → disconnected)
  • cleanup 함수로 메모리 누수 방지

3. UI: AiStreamDemo Component

채팅 인터페이스와 자동 스크롤을 구현한다.

// features/sse-demo/ui/ai-stream-demo.tsx
export function AiStreamDemo() {
  const [messages, setMessages] = useState<Message[]>([]);
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const { streamedText, connectionState, startStream } = useAiStream();

  // 자동 스크롤 (즉각적으로 하단으로)
  const scrollToBottom = useCallback(() => {
    if (scrollContainerRef.current) {
      scrollContainerRef.current.scrollTop =
        scrollContainerRef.current.scrollHeight;
    }
  }, []);

  // 메시지 변경 시 자동 스크롤
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      scrollToBottom();
    }, 0);
    return () => clearTimeout(timeoutId);
  }, [messages, streamedText]);

  const handleSubmit = () => {
    setMessages((prev) => [...prev, { role: "user", content: userInput }]);
    setUserInput("");
    startStream(); // SSE 스트리밍 시작
  };

  return (
    <Card>
      {/* 메시지 영역 */}
      <div ref={scrollContainerRef} className="h-[400px] overflow-y-auto">
        {messages.map((msg, idx) => (
          <MessageBubble key={idx} message={msg} />
        ))}

        {/* 스트리밍 중인 응답 */}
        {isStreaming && streamedText && (
          <div className="bg-secondary text-secondary-foreground">
            {streamedText}
            <span className="animate-pulse">▊</span>
          </div>
        )}
      </div>

      {/* 입력 영역 */}
      <Input value={userInput} onChange={(e) => setUserInput(e.target.value)} />
      <Button onClick={handleSubmit}>전송</Button>
    </Card>
  );
}

핵심 포인트:

  • scrollTop = scrollHeight로 안정적인 자동 스크롤 구현
  • setTimeout(..., 0)으로 DOM 업데이트 후 스크롤 보장
  • 스트리밍 중인 텍스트와 완료된 메시지 분리 표시

프로덕션 체크리스트

실제 서비스에 배포할 때 확인해야 할 사항들이다.

필수 설정:

  • ✅ HTTP/2 활성화 (Vercel, Netlify는 기본 지원)
  • ✅ 하트비트 구현 (30초 주기로 : heartbeat\n\n 전송)
  • ✅ 연결 정리 (클라이언트 disconnect 시 리소스 해제)
  • ✅ 에러 처리 (재연결 로직 및 에러 UI)
  • ✅ HTTPS 필수 (보안 및 HTTP/2 요구사항)

다중 서버 환경:

Sticky Sessions으로 로드밸런서에서 동일 서버로 라우팅하거나, Redis Pub/Sub으로 서버 간 이벤트를 동기화한다.

성능 최적화:

  • 압축: Accept-Encoding: gzip 지원
  • 연결 수 제한: 사용자당 최대 연결 수 제한
  • 타임아웃: 비활성 연결 자동 종료 (5분)

마치며

SSE는 HTTP를 기반으로 한 단순하지만 강력한 실시간 통신 기술이다. 자동 재연결, 이벤트 ID 기반 복구, HTTP/2와의 완벽한 조합으로 복잡한 WebSocket 없이도 충분히 강력한 실시간 기능을 구현할 수 있다.

단방향 데이터 푸시가 필요한 대부분의 경우, SSE는 가장 실용적인 선택이다.

  • SSE란?
  • 작동 원리
  • 이벤트 형식
  • 재연결 메커니즘
  • HTTP/2의 중요성
  • WebSocket과의 차이
  • 언제 SSE를 사용할까?
  • 언제 WebSocket을 사용할까?
  • 보안 체크리스트
  • 직접 체험해보기
  • 실전 구현: AI 스트리밍 데모 분석
  • 1. 서버: Next.js API Route
  • 2. 클라이언트: useAiStream Hook
  • 3. UI: AiStreamDemo Component
  • 프로덕션 체크리스트
  • 마치며
  • SSE란?
  • 작동 원리
  • 이벤트 형식
  • 재연결 메커니즘
  • HTTP/2의 중요성
  • WebSocket과의 차이
  • 언제 SSE를 사용할까?
  • 언제 WebSocket을 사용할까?
  • 보안 체크리스트
  • 직접 체험해보기
  • 실전 구현: AI 스트리밍 데모 분석
  • 1. 서버: Next.js API Route
  • 2. 클라이언트: useAiStream Hook
  • 3. UI: AiStreamDemo Component
  • 프로덕션 체크리스트
  • 마치며

관련 포스트

이 포스트와 관련된 다른 글들을 확인해보세요

Server-Sent Events (SSE): 간단하지만 강력한 실시간 통신

WebSocket보다 간단한 SSE로 실시간 알림과 스트리밍 구현하기. HTTP/2 기반 작동 원리부터 Next.js API Route 구현, TypeScript 커스텀 훅, 실전 데모 분석까지 완벽 가이드.

2025년 12월 22일•5분
ssereal-timetypescript+3

프론트엔드 번들 캐시버스팅을 구현해봤다

배포 후 사용자가 이전 버전을 보는 문제를 해결하기 위해 클라이언트 레벨 캐시버스팅과 CloudFront Function 기반 인프라를 구축한 과정

2025년 12월 7일•5분
frontenddeploymentcache-busting+4

CSS만으로 글리치 텍스트 만들기

JavaScript 없이 CSS keyframes, pseudo-elements, custom properties만으로 글리치 효과를 구현한다. CSS 애니메이션의 가능성과 한계를 탐구한다.

2026년 2월 16일•5분
cssanimationkeyframes+2

Comments