Model APIs

OpenAI Realtime API 연결 기준: 브라우저 음성은 WebRTC, 서버 로직은 WebSocket·sideband로 나눠야 한다

이천재 2026. 4. 30. 22:47

OpenAI Realtime API를 붙일 때 첫 질문은 "WebRTC가 좋나, WebSocket이 좋나"가 아니다. 마이크와 스피커가 어디에 있고, 표준 API key가 어디에 남아야 하며, 주문 조회나 정책 판단 같은 서버 로직을 누가 처리해야 하는지가 먼저다.

2026-04-30 기준 OpenAI 공식 문서로 보면 기본 경계는 꽤 분명하다. 브라우저나 모바일에서 사용자가 음성으로 대화한다면 WebRTC부터 본다. 서버 worker가 음성 스트림과 이벤트를 직접 처리한다면 WebSocket이 맞다. 전화번호로 들어오는 통화는 SIP path다. 답변 생성 없이 실시간 자막만 필요하면 speech-to-speech session이 아니라 transcription session을 따로 봐야 한다.

짧게 정리하면 이렇다.

  • 브라우저/모바일 음성 에이전트는 WebRTC를 기본값으로 잡는다.
  • 표준 OpenAI API key는 브라우저에 넣지 않는다. client에는 ephemeral key 또는 unified server initialization 경계를 둔다.
  • WebSocket은 server-to-server에 맞지만 audio buffer와 JSON event 처리를 직접 책임져야 한다.
  • tool 호출, 주문 조회, 내부 정책, guardrail은 client가 아니라 server sideband로 빼는 편이 안전하다.
  • 실시간 자막만 필요하면 type=transcription session을 쓴다. 이 mode는 보통 model response를 만들지 않는다.
  • 전화 상담처럼 phone number가 entry point라면 SIP trunk와 incoming webhook accept/reject flow를 별도로 설계한다.

Realtime API는 음성 채팅 하나만 뜻하지 않는다

OpenAI Realtime API overview는 Realtime API를 low-latency multimodal application용으로 설명한다. 음성 입력과 음성 출력만 있는 것이 아니라 audio, image, text input과 audio, text output을 다룰 수 있고, realtime audio transcription에도 쓰인다.

그래서 "Realtime API를 쓴다"는 말만으로는 구현 방식이 정해지지 않는다. 브라우저에서 바로 음성 대화를 만들 수도 있고, 서버에서 WebSocket으로 event를 처리할 수도 있다. 전화망에서 들어오는 call을 SIP로 받을 수도 있고, 모델 답변 없이 transcript만 stream할 수도 있다.

문제는 이 네 가지를 한 코드 path에 섞을 때 생긴다. 브라우저 데모에서 편하다는 이유로 표준 API key를 넣거나, WebSocket으로 서버를 만들면서 base64 audio buffer 처리를 빼먹거나, transcription-only 요구사항에 speech-to-speech session을 붙이면 나중에 고치기 어렵다.

브라우저 음성은 WebRTC부터 본다

OpenAI WebRTC guide는 browser나 mobile client가 Realtime model에 연결할 때 WebSocket보다 WebRTC를 권장한다. 이유는 단순하다. 브라우저의 microphone input, remote audio playback, peer connection, data channel이 모두 WebRTC의 영역에 있다.

WebRTC path에도 두 가지 초기화 방식이 있다.

방식 흐름 조심할 점
ephemeral key 서버가 /v1/realtime/client_secrets로 short-lived key를 만들고 browser가 SDP를 Realtime API에 보낸다 key 발급 endpoint와 session config를 서버에서 관리해야 한다
unified interface browser가 SDP를 app server에 보내고 server가 session config와 함께 /v1/realtime/calls에 보낸다 구현은 단순해지지만 session initialization에 서버가 critical path가 된다

둘 중 무엇을 고르든 표준 API key를 browser에 넣으면 안 된다. Realtime API reference도 client secret은 client-side environment에서 쓰는 ephemeral key이고, standard API token은 server-side only라고 설명한다. 같은 reference는 현재 client secret token expiry를 one minute로 설명한다. 이 숫자는 변할 수 있으니 배포 전 다시 확인해야 하지만, 구조상 "브라우저에 오래 살아 있는 비밀값을 둔다"는 설계는 맞지 않다.

서버 worker는 WebSocket이 맞다

WebSocket guide는 server-to-server Realtime integration에 WebSocket이 좋은 선택이라고 설명한다. backend system이 Realtime API에 직접 WebSocket으로 연결하고, 표준 API key는 secure backend server에만 남긴다.

대신 WebSocket은 낮은 수준의 인터페이스다. Realtime conversations guide는 WebRTC가 audio send/receive에 필요한 media handling을 많이 도와주지만, WebSocket audio는 input audio buffer에 base64-encoded audio를 직접 보내야 한다고 설명한다. 즉 서버 worker를 만들려면 event loop, reconnect, audio chunk append, commit, response event 처리까지 운영 코드가 가져가야 한다.

간단히 나누면 이렇게 된다.

상황 먼저 볼 path
사용자가 브라우저에서 말하고 바로 듣는다 WebRTC
backend가 call audio stream을 받아 모델 event를 직접 처리한다 WebSocket
browser media는 직접 연결하되 tool 실행은 서버에 둔다 WebRTC + sideband
전화번호로 들어오는 통화를 받는다 SIP
모델 답변 없이 transcript만 필요하다 Realtime transcription

WebSocket을 고르면 "서버니까 안전하다"에서 끝나지 않는다. audio buffer를 어떻게 append하고 commit할지, response.create를 언제 보낼지, response.done과 usage를 어디에 기록할지 정해야 한다.

tool과 내부 정책은 sideband로 분리한다

브라우저에 WebRTC를 붙였다고 해서 모든 로직을 브라우저에 둬야 하는 것은 아니다. Server controls guide는 client가 WebRTC나 SIP로 Realtime API server에 직접 연결하더라도 tool use와 business logic은 application server에 남기는 것이 좋다고 설명한다. 이를 위해 sideband control channel을 둔다.

Sideband는 같은 Realtime session에 두 개의 연결이 붙는 구조다. 하나는 사용자의 client connection이고, 다른 하나는 application server connection이다. 서버 connection은 session을 monitor하고, instructions를 업데이트하고, tool call에 응답할 수 있다.

이 경계는 실제 서비스에서 중요하다. 예를 들어 브라우저 voice agent가 "내 주문 상태 알려줘"라는 요청을 받는다고 하자. 음성 media 자체는 WebRTC가 처리해도 된다. 하지만 주문 DB credential, policy rule, refund 가능 여부 판단은 browser code에 두면 안 된다. client에는 음성 UX를 맡기고, 서버 sideband가 tool call을 받아 검증한 뒤 결과만 session에 돌려주는 구조가 더 낫다.

실시간 자막만 필요하면 transcription session이다

Realtime transcription guide는 transcription-only use case를 따로 설명한다. 마이크나 file input에서 realtime subtitles나 transcripts를 만들 수 있지만, transcription-only mode에서는 model response를 생성하지 않는다. session type도 transcription이다.

이 말은 구현 선택에서 꽤 큰 차이를 만든다. 회의 화면에 실시간 자막만 띄우려는 기능에 speech-to-speech assistant를 붙이면 불필요한 response event와 비용 경계가 생긴다. 반대로 상담 봇처럼 사용자의 말을 듣고 답변까지 해야 하는 경우라면 transcription-only session만으로는 부족하다.

OpenAI docs는 transcription session에서 conversation.item.input_audio_transcription.deltaconversation.item.input_audio_transcription.completed event를 받을 수 있다고 설명한다. gpt-4o-transcribegpt-4o-mini-transcribe는 incremental transcript를 stream할 수 있고, whisper-1은 delta event에도 full turn transcript가 들어간다고 설명한다.

전화번호가 출발점이면 SIP다

전화 상담처럼 phone number가 entry point라면 WebRTC와 WebSocket만으로는 부족하다. SIP guide는 SIP trunking provider를 통해 phone call을 IP traffic으로 바꾸고, OpenAI SIP endpoint와 incoming call webhook을 연결하는 흐름을 설명한다.

핵심은 inbound call을 webhook으로 받고, call_id를 기준으로 accept 또는 reject를 결정한다는 점이다. accept할 때 model, voice, instructions 같은 Realtime session config를 넘긴다. 세션이 열린 뒤에는 usual monitoring path를 붙일 수 있다.

SIP는 "브라우저 음성 에이전트를 전화로도 열어두자" 정도의 작은 옵션이 아니다. 번호 구매, carrier, webhook 검증, accept/reject 정책, hangup, monitoring이 같이 들어간다. 이 글의 local router도 incoming phone support scenario는 별도 sip route로 분리했다.

비용은 연결이 아니라 Response에서 주로 갈린다

Realtime costs guide는 현재 network bandwidth나 connection 자체 비용은 없고, Response가 생성될 때 input/output token 기준으로 비용이 발생한다고 설명한다. 또 Realtime conversation에서는 이전 turn의 item들이 다음 Response의 input으로 들어가므로 뒤 turn이 더 비싸질 수 있다.

그래서 연결 방식만 바꾼다고 비용 문제가 끝나지 않는다. WebRTC든 WebSocket이든 사용자가 계속 대화하고 Response가 계속 만들어지면 usage가 쌓인다. 비용을 보려면 response.done event의 usage를 저장하고, session이 길어질 때 어떤 context를 유지할지 따로 정해야 한다.

또 하나의 작은 함정은 voice 설정이다. API reference는 model이 audio output을 한 번 낸 뒤에는 그 session에서 voice를 변경할 수 없다고 설명한다. 운영 UI에서 voice를 바꾸게 하려면 첫 audio output 전에 설정을 끝내거나 새 session을 열어야 한다.

로컬 routing gate 결과

이번 run에서는 OpenAI API를 실제 호출하지 않았다. 마이크도 열지 않았고, WebRTC peer connection이나 SIP trunk도 만들지 않았다. 대신 공식 문서의 연결 조건을 deterministic routing gate로 바꿔 8개 scenario를 평가했다.

scenario score route recommendation
browser-voice-agent 100 webrtc use_webrtc_with_ephemeral_or_unified_server_initialization
browser-voice-agent-with-order-tool 95 webrtc_plus_sideband use_webrtc_for_media_and_sideband_for_private_tool_control
backend-audio-worker 100 websocket use_server_to_server_websocket
backend-worker-no-audio-buffer 82 websocket use_server_to_server_websocket
live-caption-browser 100 realtime_transcription use_realtime_transcription_session_without_model_response
public-demo-leaks-standard-key 30 webrtc fix_blockers_before_realtime
incoming-phone-support 100 sip use_sip_with_webhook_accept_reject_and_optional_sideband
next-day-podcast-transcript 85 not_realtime use_non_realtime_audio_transcription_or_batch_pipeline

가장 중요한 blocker는 public-demo-leaks-standard-key였다. 브라우저에서 표준 API key를 쓰는 설계는 WebRTC와 WebSocket을 비교하기 전에 막아야 한다. backend-worker-no-audio-buffer는 WebSocket route로 남았지만, base64 audio buffer 처리가 없어서 warning을 받았다.

반대로 browser-voice-agent-with-order-tool은 WebRTC가 틀린 것이 아니었다. media path는 WebRTC가 맞지만, 주문 조회 tool과 내부 정책은 sideband server control로 분리해야 했다. 같은 Realtime API라도 media plane과 control plane을 나눠야 한다는 뜻이다.

구현 전 체크리스트

OpenAI Realtime API를 붙이기 전에 아래 질문을 먼저 본다.

  1. 사용자가 브라우저나 모바일에서 마이크와 스피커를 직접 쓰는가.
  2. client에 표준 API key가 들어가지 않는 구조인가.
  3. ephemeral key 또는 unified interface를 발급하는 server endpoint가 있는가.
  4. 서버가 직접 audio stream과 event를 처리해야 한다면 WebSocket audio buffer 처리가 준비됐는가.
  5. tool, DB credential, policy rule, guardrail이 client code에 들어가지 않는가.
  6. WebRTC/SIP direct session에 server control이 필요하면 sideband를 설계했는가.
  7. 답변 생성이 필요한가, transcript만 필요한가.
  8. transcript-only라면 type=transcription session과 delta/completed event 처리를 따로 잡았는가.
  9. 전화번호가 entry point라면 SIP trunk, incoming webhook, accept/reject policy가 있는가.
  10. response.done usage를 저장하고 session이 길어질 때 비용이 늘어나는 구조를 이해했는가.
  11. voice를 session 중간에 바꿔야 하는 UI라면 첫 audio output 전 설정 또는 새 session 정책을 정했는가.

이 중 2번이 비어 있으면 구현을 멈추는 편이 낫다. 4번이 비어 있으면 WebSocket은 아직 이르다. 5번과 6번이 비어 있으면 voice demo는 돌아가도 실제 서비스의 tool 실행 경계가 흔들린다.

같이 보면 좋은 글

참고 자료

실행 로그 첨부

민감정보를 제거한 공개용 로그와 실험 스크립트만 아래에 첨부한다.

  • wn116-realtime-connection-router-public.json
  • wn116-realtime-connection-router-summary-public.md
  • wn116-realtime-connection-router-public.py

wn116-realtime-connection-router-public.json
0.01MB
wn116-realtime-connection-router-summary-public.md
0.00MB
wn116-realtime-connection-router-public.py
0.01MB