실시간 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의 글로벌 게이트웨이를 활용하면 기본 연결 안정성이 높아져 재연결 빈도를 최소화할 수 있습니다.

핵심 포인트:

👉 HolySheep AI 가입하고 무료 크레딧 받기