OpenAI Realtime API로 음성 챗봇 구현하며 겪은 WebSocket 연결 이슈

배경

회사에서 고객 상담용 음성 챗봇 프로토타입을 만들게 되었다. OpenAI의 Realtime API(베타)가 음성 입출력을 직접 처리할 수 있다는 점에서 선택했다. GPT-4o 기반이라 응답 품질도 괜찮았다.

문제 상황

초기 구현은 순조로웠으나, 실제 테스트 중 30초~1분 사이 연결이 자주 끊기는 현상이 발생했다. WebSocket 연결이 갑자기 close 이벤트를 받으며 종료되었다.

const ws = new WebSocket(
  'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01',
  {
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'OpenAI-Beta': 'realtime=v1'
    }
  }
);

ws.on('close', (code) => {
  console.log(`Connection closed: ${code}`);
});

원인 분석

  1. Keep-alive 부재: WebSocket은 idle 상태가 길어지면 중간 프록시나 로드밸런서에서 연결을 끊을 수 있다.
  2. 오디오 버퍼 처리: 클라이언트에서 마이크 입력을 청크 단위로 보낼 때 버퍼 크기가 일정하지 않아 API 측에서 처리 오류가 발생했다.

해결 방법

1. Ping-Pong 메커니즘

20초마다 ping을 보내 연결을 유지했다.

let pingInterval;

ws.on('open', () => {
  pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 20000);
});

ws.on('close', () => {
  clearInterval(pingInterval);
});

2. 오디오 버퍼 정규화

PCM16 포맷으로 고정하고, 100ms 단위로 청크를 나눠 전송했다.

const SAMPLE_RATE = 24000;
const CHUNK_SIZE = SAMPLE_RATE * 0.1 * 2; // 100ms

function sendAudioChunk(audioData) {
  const event = {
    type: 'input_audio_buffer.append',
    audio: audioData.toString('base64')
  };
  ws.send(JSON.stringify(event));
}

3. 재연결 로직

연결 끊김 시 자동 재연결을 구현했다.

function connect() {
  const ws = new WebSocket(/* ... */);
  
  ws.on('close', () => {
    setTimeout(() => {
      console.log('Reconnecting...');
      connect();
    }, 3000);
  });
  
  return ws;
}

결과

위 조치 후 5분 이상 안정적으로 대화가 유지되었다. Realtime API는 아직 베타라 문서가 부족한 편이지만, 음성 latency가 낮아 실시간 상담 용도로는 충분히 활용 가능하다고 판단했다.

참고

  • OpenAI Realtime API 문서
  • WebSocket RFC 6455