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