AI 대화형 서비스와 실시간 데이터 전송을 위한 SSE 기술 가이드

최근 ChatGPT와 같은 AI 대화형 서비스가 급격히 성장하면서, 서버에서 클라이언트로 데이터를 실시간 스트리밍하는 기술인 SSE(Server-Sent Events)가 다시금 주목받고 있습니다. AI 모델이 생성하는 텍스트를 한 자씩 사용자에게 보여주기 위해 가장 효율적인 방식 중 하나이기 때문입니다.

1. SSE의 개념과 주요 활용 사례

SSE는 브라우저의 EventSource API를 사용하여 서버와 클라이언트 간에 유지되는 단방향 HTTP 연결입니다. 서버는 text/event-stream 형식으로 데이터를 지속적으로 푸시하며, HTTP/1.1부터 표준으로 지원됩니다.

  • 적합한 시나리오:
    • AI 챗봇의 실시간 텍스트 생성 스트리밍
    • 실시간 주가 지수나 대시보드 모니터링
    • 시스템 알림 및 푸시 메시지
    • 로그 스트리밍 및 상태 업데이트
  • 부적합한 시나리오:
    • 멀티플레이어 게임과 같이 고빈도 양방향 통신이 필요한 경우 (WebSocket 권장)
    • 바이너리 데이터를 직접 전송해야 하는 경우

2. 통신 방식 비교

특징 SSE WebSocket Long Polling
방향성 단방향 (Server → Client) 양방향 (Full Duplex) 단방향 (요청-응답 방식)
프로토콜 HTTP (표준) WebSocket 전용 프로토콜 HTTP
재연결 기본 자동 재연결 지원 직접 구현 필요 루프 기반 구현
데이터 형식 텍스트 기반 텍스트 및 바이너리 제한 없음

3. SSE 메시지 프로토콜 구조

서버가 전송하는 데이터는 특정 필드 형식을 갖춘 텍스트여야 하며, 각 이벤트는 반드시 빈 줄(\n\n)로 구분되어야 합니다.

  • data: 메시지의 실제 내용. 여러 줄 사용 시 자동으로 병합됩니다.
  • id: 이벤트 고유 식별자. 연결이 끊겼을 때 클라이언트가 Last-Event-ID 헤더를 통해 복구 지점을 서버에 알릴 수 있습니다.
  • event: 사용자 정의 이벤트 이름. 클라이언트에서 특정 리스너를 실행할 수 있습니다.
  • retry: 재연결을 시도할 대기 시간(ms).
  • : 콜론으로 시작하는 행은 주석으로 처리되어 클라이언트가 무시하며, 연결 유지(Keep-alive) 용도로 사용됩니다.

4. 실무 구현 예제

서버측 구현 (Node.js/Express)

import express from 'express';
const app = express();

app.get('/api/stream', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // 최초 연결 시 주석 메시지 전송 (프록시 서버 연결 유지)
    res.write(': connection established\n\n');

    let messageCount = 0;
    const streamInterval = setInterval(() => {
        messageCount++;
        const responseData = {
            content: `AI 응답 데이터 ${messageCount}`,
            timestamp: new Date().toISOString()
        };

        res.write(`id: ${messageCount}\n`);
        res.write(`event: chatUpdate\n`);
        res.write(`data: ${JSON.stringify(responseData)}\n\n`);

        if (messageCount >= 10) {
            clearInterval(streamInterval);
            res.end();
        }
    }, 1000);

    req.on('close', () => {
        clearInterval(streamInterval);
        console.log('클라이언트 연결 종료');
    });
});

app.listen(4000, () => console.log('SSE 서버 가동: 4000포트'));

클라이언트측 구현 (JavaScript)

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

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

eventSource.addEventListener('chatUpdate', (event) => {
    const parsedData = JSON.parse(event.data);
    console.log('수신 데이터:', parsedData.content);
});

eventSource.onerror = (error) => {
    console.error('SSE 에러 발생:', error);
    // 연결 실패 시 브라우저가 자동으로 재연결을 시도함
};

5. AI 스트리밍 서비스에서의 성능 최적화

AI 응답처럼 텍스트를 실시간으로 렌더링할 때, 전체 텍스트를 매번 다시 렌더링하면 브라우저의 성능 저하(Reflow/Repaint)를 유발합니다. 이를 해결하기 위한 전략은 다음과 같습니다.

  1. 증분 업데이트(Incremental Update): 서버에서 전체 마크다운이 아닌, 새로 생성된 '토큰'이나 '델타(Delta)' 데이터만 전송합니다. 클라이언트는 기존 DOM 요소 뒤에 새 텍스트를 추가(Append)하는 방식으로 처리하여 렌더링 부하를 줄입니다.
  2. Web Worker 활용: 복잡한 마크다운 파싱이나 구문 강조(Syntax Highlighting) 작업을 메인 스레드가 아닌 Web Worker에서 처리하여 UI 끊김 현상을 방지합니다.
  3. 렌더링 배치(Batching): 짧은 간격으로 들어오는 데이터를 requestAnimationFrame을 사용해 묶어서 처리함으로써 불필요한 브라우저 페인팅 횟수를 줄입니다.
  4. Keep-Alive 전략: 프록시 서버나 로드 밸런서에서 타임아웃으로 인해 연결이 끊기지 않도록 주기적으로 빈 주석(: \n\n)을 전송하여 활성 상태를 유지합니다.

태그: SSE Server-Sent-Events Real-time-Streaming HTTP Node.js

9월 6일 19:46에 게시됨