OpenAI API 스트리밍 응답 처리 중 연결 끊김 문제 해결

문제 상황

챗봇 서비스에서 OpenAI API의 스트리밍 응답을 처리하는 중 사용자가 긴 응답을 받을 때 간헐적으로 연결이 끊기는 이슈가 발생했다. 특히 1분 이상 소요되는 응답에서 문제가 두드러졌다.

원인 분석

  1. Vercel 함수 타임아웃: 무료 플랜의 10초, Pro 플랜의 60초 제한
  2. 네트워크 계층 타임아웃: 중간 프록시나 로드밸런서의 idle timeout
  3. 클라이언트 측 재연결 로직 부재

해결 방법

1. Edge Function으로 전환

// app/api/chat/route.ts
export const runtime = 'edge';

export async function POST(req: Request) {
  const { messages } = await req.json();
  
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages,
      stream: true,
    }),
  });

  return new Response(response.body, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

2. 클라이언트 재연결 처리

const fetchWithRetry = async (retries = 3) => {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        body: JSON.stringify({ messages }),
        signal: AbortSignal.timeout(120000),
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader!.read();
        if (done) break;
        
        const chunk = decoder.decode(value);
        processChunk(chunk);
      }
      
      return;
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
};

3. Heartbeat 메커니즘

OpenAI API는 자체적으로 주기적인 데이터를 보내지만, 필요시 서버에서 명시적인 keep-alive를 추가할 수 있다.

const encoder = new TextEncoder();
const heartbeat = setInterval(() => {
  controller.enqueue(encoder.encode(': heartbeat\n\n'));
}, 15000);

결과

Edge Function으로 전환 후 타임아웃 문제가 해결되었고, 재연결 로직으로 네트워크 불안정 상황에서도 사용자 경험이 개선되었다. 모니터링 결과 평균 응답 완료율이 94%에서 99.2%로 향상되었다.