실시간 AI 스트리밍 서비스에서 네트워크 단절은 피할 수 없는 현실입니다. 저는 이커머스 플랫폼에서 AI 고객 챗봇을 운영하면서 1초도 중단되지 않는 스트리밍 서비스를 구현해야 했고, 이 과정에서 SSE 재연결 메커니즘의 모든 함정을 경험했습니다.
왜 SSE 재연결이 중요한가?
Server-Sent Events(SSE)는 AI 응답을 실시간으로 스트리밍하는 표준 방법입니다. 그러나 네트워크 불안정, 서버 재시작, 로드밸런서 타임아웃 등으로 연결이 끊어질 수 있습니다. 적절한 재연결 전략 없이는:
- 사용자가 응답 중간에 빈 화면을 보게 됨
- 중복 요청으로 비용 증가
- 치명적用户体验崩溃
HolySheep AI의 글로벌 게이트웨이(지금 가입)를 사용하면 200ms 미만의 지연 시간으로 안정적인 연결을 제공하지만, 클라이언트 측 재연결 로직도 필수적입니다.
기본 SSE 구현과 재연결 구조
class SSEReconnectManager {
private baseURL: string;
private apiKey: string;
private retryCount: number = 0;
private maxRetries: number = 10;
private baseDelay: number = 1000; // 1초
private maxDelay: number = 30000; // 30초
private controller: AbortController | null = null;
private eventSource: EventSource | null = null;
private onMessageCallback: ((data: string) => void) | null = null;
private onErrorCallback: ((error: Error) => void) | null = null;
private lastEventId: string | null = null;
private reconnecting: boolean = false;
constructor(apiKey: string) {
this.apiKey = apiKey;
// HolySheep AI 공식 엔드포인트
this.baseURL = 'https://api.holysheep.ai/v1';
}
// 지수 백오프 딜레이 계산
private calculateBackoffDelay(): number {
const delay = Math.min(
this.baseDelay * Math.pow(2, this.retryCount),
this.maxDelay
);
// 제이itter 추가 (0.5~1.5배)
const jitter = 0.5 + Math.random();
return Math.floor(delay * jitter);
}
// SSE 스트림 연결
async connectStream(
endpoint: string,
payload: object,
onMessage: (data: string) => void,
onError: (error: Error) => void
): Promise {
this.onMessageCallback = onMessage;
this.onErrorCallback = onError;
await this.establishConnection(endpoint, payload);
}
private async establishConnection(
endpoint: string,
payload: object,
isReconnect: boolean = false
): Promise {
this.controller = new AbortController();
// HolySheep AI SSE 엔드포인트 (streaming chat)
const url = ${this.baseURL}/chat/completions;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${this.apiKey},
},
body: JSON.stringify({
...payload,
stream: true,
// 재연결 시 마지막 이벤트 ID로 이어서 받기
...(this.lastEventId && isReconnect && {
stream_options: { include_usage: true }
})
}),
signal: this.controller.signal,
});
if (!response.ok) {
throw new Error(HTTP ${response.status}: ${response.statusText});
}
// 연결 성공 시 리트라이 카운터 리셋
this.retryCount = 0;
this.reconnecting = false;
// 스트림 읽기
await this.readStream(response.body!);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('연결이 의도적으로 종료되었습니다');
return;
}
this.handleConnectionError(error as Error);
}
}
private async readStream(body: ReadableStream): Promise {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(data);
// 마지막 이벤트 ID 추적
if (parsed.id) {
this.lastEventId = parsed.id;
}
this.onMessageCallback?.(data);
} catch (e) {
console.warn('JSON 파싱 실패:', data);
}
}
}
}
} finally {
reader.releaseLock();
}
}
private handleConnectionError(error: Error): void {
console.error('SSE 연결 오류:', error.message);
this.onErrorCallback?.(error);
if (this.retryCount >= this.maxRetries) {
console.error('최대 재연결 시도 횟수 초과');
return;
}
const delay = this.calculateBackoffDelay();
console.log(${delay}ms 후 재연결 시도... (${this.retryCount + 1}/${this.maxRetries}));
this.retryCount++;
this.reconnecting = true;
// 지수 백오프 후 재연결
setTimeout(() => {
this.establishConnection(
'/chat/completions',
{},
true
);
}, delay);
}
// 연결 종료
disconnect(): void {
this.controller?.abort();
this.retryCount = 0;
this.reconnecting = false;
}
// 연결 상태 확인
isReconnecting(): boolean {
return this.reconnecting;
}
getRetryCount(): number {
return this.retryCount;
}
}
실전 사용 예제: 이커머스 AI 고객 서비스
제가 개발한 이커머스 AI 고객 서비스는 상품 문의, 주문 추적, 반품 처리 등을 실시간으로 처리합니다. 트래픽이 몰리는 시간대에 안정적으로 동작해야 했기 때문에 SSE 재연결이 필수적이었습니다.
// HolySheep AI SSE 재연결 관리자 인스턴스 생성
const sseManager = new SSEReconnectManager('YOUR_HOLYSHEEP_API_KEY');
// 메시지 파싱 유틸리티
function parseStreamChunk(rawData: string) {
try {
const parsed = JSON.parse(rawData);
return {
id: parsed.id,
role: parsed.choices?.[0]?.delta?.role,
content: parsed.choices?.[0]?.delta?.content || '',
done: parsed.choices?.[0]?.finish_reason === 'stop',
usage: parsed.usage
};
} catch {
return null;
}
}
// 이커머스 AI 챗봇 서비스
class EcommerceChatService {
private messageBuffer: string = '';
private isStreaming: boolean = false;
async sendMessage(userId: string, message: string): Promise {
if (this.isStreaming) {
console.warn('이전 요청이 아직 처리 중입니다');
return;
}
this.isStreaming = true;
this.messageBuffer = '';
// UI 업데이트 콜백
const onMessage = (rawData: string) => {
const chunk = parseStreamChunk(rawData);
if (chunk && chunk.content) {
this.messageBuffer += chunk.content;
this.updateUI(this.messageBuffer);
}
};
// 오류 처리 콜백
const onError = (error: Error) => {
console.error('AI 스트리밍 오류:', error);
this.showError('연결이 일시적으로 끊어졌습니다. 자동 재연결 중...');
};
try {
await sseManager.connectStream(
'/chat/completions',
{
model: 'gpt-4.1',
messages: [
{ role: 'system', content: '당신은 친절한 이커머스 고객 서비스 챗봇입니다.' },
{ role: 'user', content: message }
],
max_tokens: 1000,
temperature: 0.7
},
onMessage,
onError
);
} finally {
this.isStreaming = false;
}
}
private updateUI(text: string): void {
const chatElement = document.getElementById('chat-response');
if (chatElement) {
chatElement.textContent = text;
}
}
private showError(message: string): void {
const errorElement = document.getElementById('error-message');
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
}
}
// 사용 예시
const chatService = new EcommerceChatService();
// 재연결 상태 모니터링
setInterval(() => {
if (sseManager.isReconnecting()) {
console.log(재연결 중... 시도 횟수: ${sseManager.getRetryCount()});
}
}, 1000);
고급 기능: 자동 재연결 정책 커스터마이징
// 고급 SSE 재연결 관리자 (커스터마이징 가능)
class AdvancedSSEManager {
private config: SSERetryConfig;
private retryState: RetryState;
private eventHandlers: Map;
constructor(config: Partial = {}) {
this.config = {
maxRetries: config.maxRetries ?? 10,
baseDelay: config.baseDelay ?? 1000,
maxDelay: config.maxDelay ?? 30000,
backoffFactor: config.backoffFactor ?? 2.0,
jitterFactor: config.jitterFactor ?? 0.3,
retryableStatuses: config.retryableStatuses ?? [408, 429, 500, 502, 503, 504],
timeoutMs: config.timeoutMs ?? 60000,
};
this.retryState = {
count: 0,
lastAttempt: null,
totalDowntime: 0,
errors: []
};
this.eventHandlers = new Map();
}
// 지수 백오프 + 지터 계산
private calculateDelay(): number {
const exponentialDelay = this.config.baseDelay *
Math.pow(this.config.backoffFactor, this.retryState.count);
const clampedDelay = Math.min(exponentialDelay, this.config.maxDelay);
// 지터 적용 (concurrency 충돌 방지)
const jitterRange = clampedDelay * this.config.jitterFactor;
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
return Math.floor(clampedDelay + jitter);
}
// 재연결 가능 상태 확인
private isRetryable(error: HTTPError): boolean {
// 네트워크 오류는 항상 재시도
if (error.type === 'network') return true;
// 지정된 HTTP 상태码만 재시도
return this.config.retryableStatuses.includes(error.status);
}
// 상태 이벤트 발생
private emit(event: string, data: any): void {
const handler = this.eventHandlers.get(event);
if (handler) {
handler(data);
}
}
// 이벤트 리스너 등록
on(event: string, handler: Function): void {
this.eventHandlers.set(event, handler);
}
// 재연결 실행
async retry(error: Error): Promise {
if (this.retryState.count >= this.config.maxRetries) {
this.emit('retry_exhausted', this.retryState);
return false;
}
const delay = this.calculateDelay();
this.retryState.count++;
this.retryState.lastAttempt = new Date();
this.retryState.errors.push({
message: error.message,
timestamp: Date.now()
});
this.emit('retry_scheduled', {
attempt: this.retryState.count,
delay,
error: error.message
});
await this.sleep(delay);
return true;
}
// 통계 정보 반환
getStats(): RetryStats {
return {
attempts: this.retryState.count,
totalDowntime: this.retryState.totalDowntime,
lastError: this.retryState.errors[this.retryState.errors.length - 1],
errorCount: this.retryState.errors.length
};
}
// 상태 리셋
reset(): void {
this.retryState = {
count: 0,
lastAttempt: null,
totalDowntime: 0,
errors: []
};
}
private sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// HolySheep AI 재연결 정책 예시
const holySheepRetryPolicy = new AdvancedSSEManager({
maxRetries: 8,
baseDelay: 500, // 초기 딜레이 500ms
maxDelay: 15000, // 최대 15초
backoffFactor: 1.5, // 1.5배 증가
jitterFactor: 0.2, // ±20% 지터
retryableStatuses: [408, 429, 500, 502, 503, 504],
timeoutMs: 30000
});
// 이벤트 리스너 설정
holySheepRetryPolicy.on('retry_scheduled', (data) => {
console.log([HolySheep AI] 재연결 시도 ${data.attempt}회차, ${data.delay}ms 후);
updateConnectionStatus(재연결 중... (${data.attempt}회차));
});
holySheepRetryPolicy.on('retry_exhausted', (stats) => {
console.error('[HolySheep AI] 재연결 실패:', stats);
showFatalError('AI 서비스 연결에 실패했습니다. 페이지를 새로고침해주세요.');
});
성능 최적화: HolySheep AI 게이트웨이 활용
HolySheep AI를 사용하면 재연결 빈도를 줄일 수 있습니다. 글로벌 CDN을 통한 200ms 이하의 지연 시간과 자동 장애 조치(failover) 기능이 포함되어 있기 때문입니다.
| 모델 | 가격 ($/MTok) | 재연결 시 최적 지연 |
|---|---|---|
| GPT-4.1 | $8.00 | ~250ms |
| Claude Sonnet 4.5 | $15.00 | ~180ms |
| Gemini 2.5 Flash | $2.50 | ~120ms |
| DeepSeek V3.2 | $0.42 | ~200ms |
자주 발생하는 오류와 해결
1. SSE 연결이 즉시 종료되는 문제
// ❌ 잘못된 접근: CORS 또는 헤더 누락
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Authorization 헤더 누락!
},
// ...
});
// ✅ 올바른 접근: Authorization 필수
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${apiKey} // HolySheep AI 키
},
body: JSON.stringify({
model: 'gpt-4.1',
messages: [...],
stream: true // 스트리밍 활성화 필수
}),
signal: controller.signal
});
// 추가 검증: 응답이 스트림인지 확인
if (!response.headers.get('content-type')?.includes('text/event-stream')) {
const errorData = await response.json();
throw new Error(SSE 오류: ${errorData.error?.message || response.statusText});
}
2. AbortController가 재연결을 방해하는 문제
// ❌ 잘못된 접근: 재연결 시 이전 controller 사용
class BrokenSSEManager {
private controller = new AbortController();
async connect() {
await fetch(url, { signal: this.controller.signal }); // 문제!
}
async reconnect() {
// 이전 AbortController가 여전히 동작 중
await fetch(url, { signal: this.controller.signal }); // 또 문제!
}
}
// ✅ 올바른 접근: 새 AbortController 생성
class FixedSSEManager {
private currentController: AbortController | null = null;
async connect() {
this.currentController = new AbortController(); // 새 인스턴스
await fetch(url, { signal: this.currentController.signal });
}
async reconnect() {
// 이전 연결 완전 종료
this.currentController?.abort();
this.currentController = new AbortController(); // 새 인스턴스
await fetch(url, { signal: this.currentController.signal });
}
disconnect() {
this.currentController?.abort();
this.currentController = null;
}
}
3. 지수 백오프 딜레이 계산 오버플로우
// ❌ 잘못된 접근: Number.MAX_SAFE_INTEGER 초과 위험
private calculateDelay(): number {
return this.baseDelay * Math.pow(2, this.retryCount); // 위험!
// retryCount=1000이면 Infinity 발생
}
// ✅ 올바른 접근: 안전하게 클램핑
private calculateDelay(): number {
// 1. 지수 증가 계산
let delay = this.baseDelay * Math.pow(2, this.retryCount);
// 2. 안전 상한 적용 (Number.MAX_SAFE_INTEGER 방지)
if (!Number.isFinite(delay)) {
delay = this.maxDelay;
}
// 3. 상한/하한 클램핑
delay = Math.max(this.minDelay, Math.min(delay, this.maxDelay));
// 4. 지터 추가
const jitter = delay * (0.5 + Math.random());
return Math.floor(jitter);
}
// 실제 테스트 코드
const delayCalculator = new SafeDelayCalculator({
baseDelay: 1000,
maxDelay: 30000,
minDelay: 500
});
console.log(delayCalculator.calculate(0)); // ~750ms
console.log(delayCalculator.calculate(5)); // ~32000ms
console.log(delayCalculator.calculate(100)); // 30000ms (클램핑됨)
결론
SSE 재연결 메커니즘은 안정적인 AI 스트리밍 서비스의 핵심입니다. 지수 백오프와 지터를 적절히 조합하면 서버 부하를 줄이면서도 빠른 복구를 달성할 수 있습니다. HolySheep AI의 글로벌 게이트웨이를 활용하면 기본 연결 안정성이 높아져 재연결 빈도를 최소화할 수 있습니다.
핵심 포인트:
- 지수 백오프(Exponential Backoff)로 점진적 딜레이 증가
- 지터(Jitter) 추가를 통한 thundering herd 방지
- AbortController를 재연결마다 새로 생성
- 재연결 상태를 실시간으로 사용자에게 표시
- HolySheep AI 게이트웨이로 기본 연결 품질 확보