Next.js + Flask + OpenAI Realtime API: WebRTC 음성 통화 구현기

Wonhyo LEE·2025년 8월 6일
0
post-thumbnail

최근 OpenAI에서 공개한 Realtime Voice API를 활용해, WebRTC 기반 음성 통신 애플리케이션을 구현해봤습니다. 이 글에서는 Next.js (클라이언트 & 서버 라우트), Flask (백엔드), 그리고 OpenAI Realtime API를 연동하여 실시간 음성 통신을 구축한 과정을 설명합니다.


목표

  • WebRTC를 사용해 브라우저에서 마이크 오디오를 캡처하고
  • OpenAI의 음성 모델에 실시간으로 전송하고
  • 모델의 음성 응답을 받아 오디오로 재생

이 모든 과정을 브라우저 기반으로 구현합니다.


전체 아키텍처

Next.js 클라이언트
     ↓ [POST /api/realtime/ephemeral]
Next.js 서버 (NextResponse)
     ↓ [POST /realtime/ephemeral]
Flask 서버 (requests.post)
     ↓
OpenAI Realtime API (/v1/realtime/sessions)
     ↓
Ephemeral token 발급 후 응답 전달

클라이언트는 OpenAI와 직접 연결하지 않고, 백엔드를 거쳐 ephemeral token만 발급받은 뒤 WebRTC로 연결합니다.


왜 이런 구조인가?

이유설명
🔐 OpenAI API Key 보호클라이언트에 노출되면 안 되므로 서버에서 처리
⏱ Ephemeral 토큰짧은 TTL(수분) → 연결 직전에 발급해야 안전
🧰 연결 설정 커스터마이징voice, 포맷, VAD 설정 등을 서버에서 통제 가능
🧾 로깅 & 디버깅Flask에서 로그를 남겨 문제 추적 용이

Flask: /api/realtime/ephemeral 구현

@app.route("/api/realtime/ephemeral", methods=["POST"])
def create_realtime_ephemeral():
    try:
        body = {
            "model": OPENAI_REALTIME_MODEL,
            "voice": OPENAI_REALTIME_VOICE,
            "modalities": ["text", "audio"],
            "input_audio_format": "pcm16",
            "output_audio_format": "pcm16",
            "turn_detection": {"type": "server_vad", "silence_duration_ms": 600},
            "instructions": "항상 한국어로 1~2문장으로 간결히 말해 주세요.",
        }

        resp = requests.post(
            "https://api.openai.com/v1/realtime/sessions",
            headers={
                "Authorization": f"Bearer {OPENAI_API_KEY}",
                "Content-Type": "application/json",
                "OpenAI-Beta": "realtime=v1",
            },
            json=body,
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()

        client_secret = data.get("client_secret", {})
        return jsonify({
            "client_secret": client_secret.get("value"),
            "expires_at": client_secret.get("expires_at"),
            "model": data.get("model", OPENAI_REALTIME_MODEL),
            "voice": OPENAI_REALTIME_VOICE,
        })

    except requests.HTTPError as e:
        logging.error("Realtime ephemeral HTTP error: %s", e.response.text)
        return jsonify({"error": e.response.text}), e.response.status_code
    except Exception as e:
        logging.exception("Realtime ephemeral error")
        return jsonify({"error": str(e)}), 500

✅ Flask는 OpenAI 세션 생성 API를 호출하여 ephemeral token을 생성하고, 클라이언트가 WebRTC로 연결할 수 있도록 전달합니다.

ephemeral token을 사용한 이유

OpenAI Realtime API의 보안을 위해서입니다. API Key를 클라이언트에 직접 노출하지 않고, 서버에서 짧은 TTL을 가진 임시 토큰(Ephemeral Token)을 발급해서 브라우저가 WebRTC 연결 직전에만 사용할 수 있게 합니다.
이렇게 하면 Key 유출 위험을 줄이고, 토큰이 만료되면 재사용이 불가능하므로 보안성이 높아집니다.


Next.js 서버 라우트 /api/realtime/ephemeral

import { NextResponse } from 'next/server';

export async function POST() {
  try {
    const origin = process.env.NEXT_PUBLIC_API_HOST;
    const res = await fetch(`${origin}/realtime/ephemeral`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({})
    });

    const text = await res.text();
    try {
      const json = JSON.parse(text);
      return NextResponse.json(json, { status: res.status });
    } catch {
      return new NextResponse(text, { status: res.status });
    }
  } catch (err: any) {
    return NextResponse.json({ error: String(err?.message ?? err) }, { status: 500 });
  }
}

✅ Next.js는 클라이언트에서 요청을 받아 Flask 서버에 proxy 요청을 보내고, 응답을 전달합니다.


Next.js 클라이언트 코드 핵심 흐름

전체 목적 요약

이 코드는 다음 기능을 수행합니다:

  1. 마이크 접근 후 음성 입력
  2. WebRTC로 OpenAI Realtime API와 연결
  3. 내 마이크 음성을 서버에 실시간 전달
  4. OpenAI가 처리 후 음성 응답 → audio 태그로 재생
  5. 마이크 음량을 실시간 시각화
  6. 연결 / 음소거 / 해제 기능 + 로그 기록

코드 분석: 기능별 설명

1. 상태 및 참조 변수 정의

const [connecting, setConnecting] = useState(false);
const [connected, setConnected] = useState(false);
const [muted, setMuted] = useState(false);
const [logs, setLogs] = useState<string[]>([]);
  • UI 상태를 제어하는 상태 변수들
const pcRef = useRef<RTCPeerConnection | null>(null);
const localStreamRef = useRef<MediaStream | null>(null);
const remoteAudioRef = useRef<HTMLAudioElement | null>(null);
  • WebRTC 커넥션과 스트림, 재생용 오디오 태그에 접근하기 위한 참조

2. 오디오 레벨 미터 관련

const canvasRef = useRef<HTMLCanvasElement | null>(null);
const audioCtxRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const rafRef = useRef<number | null>(null);
  • 마이크 음량 시각화를 위한 Web Audio API 관련 참조들

3. 로그 기록 유틸리티

function log(s: string) {
  setLogs((prev) => [s, ...prev].slice(0, 200));
}
  • 로그를 최근 순으로 저장하고 200줄까지만 유지합니다

4. Ephemeral Token 발급

async function getEphemeral(): Promise<{ client_secret: string }> {
  const res = await fetch('/api/realtime/ephemeral', { method: 'POST' });
  const data = await res.json();
  return { client_secret: data.client_secret };
}
  • Next.js 서버 라우트를 통해 Flask → OpenAI로 토큰을 요청

5. startAudioMeter()

function startAudioMeter(stream: MediaStream) {
  const ctx = new AudioContext();
  const analyser = ctx.createAnalyser();
  const source = ctx.createMediaStreamSource(stream);
  source.connect(analyser);
  // 캔버스에 레벨 그리기 로직 생략
}
  • 마이크 볼륨을 분석하고 캔버스에 시각화

6. connect()

const local = await navigator.mediaDevices.getUserMedia({ audio: true });
const pc = new RTCPeerConnection();
local.getTracks().forEach((t) => pc.addTrack(t, local));
  • 마이크 스트림을 획득하고 WebRTC에 트랙 추가
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
  • WebRTC offer 생성
const { client_secret } = await getEphemeral();
  • OpenAI에 인증할 토큰 발급
const resp = await fetch('https://api.openai.com/v1/realtime?...', { headers, body: offer.sdp });
const answerSdp = await resp.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
  • Offer를 OpenAI에 전송하고 Answer SDP 받아 설정 → 연결 완료

7. disconnect()

pc.getSenders().forEach((s) => s.track?.stop());
pc.close();
  • 커넥션 및 마이크 정리, 상태 초기화

8. toggleMute()

local.getAudioTracks().forEach((t) => (t.enabled = !muted));
  • enabled 속성으로 음소거 토글

9. useEffect

useEffect(() => () => disconnect(), []);
  • 컴포넌트 unmount 시 정리 수행

10. UI 구성

  • <button>: 연결, 음소거, 해제
  • <canvas>: 오디오 레벨 미터
  • <audio>: 모델의 음성 응답 재생
  • <pre>: 로그 출력 (useState, useRef 포함)
기능설명
상태 관리connecting, connected, muted, logs 등으로 UI 상태 제어
마이크 접근navigator.mediaDevices.getUserMedia() 사용
WebRTC 연결RTCPeerConnection 객체로 offer/answer 및 ICE 설정
서버 인증 요청/api/realtime/ephemeral 호출로 ephemeral 발급
마이크 음성 시각화AudioContext + AnalyserNode + Canvas 조합
마이크 음소거track.enabled = false 방식으로 제어
연결 해제트랙 및 커넥션 정리, 상태 초기화

전체 동작 흐름 요약

[1] 마이크 열기
 ↓
[2] WebRTC 연결 객체 생성
 ↓
[3] Offer 생성 → OpenAI로 전송
 ↓
[4] Answer 수신 → 연결 완료
 ↓
[5] 내 마이크 음성 → 실시간 스트리밍
 ↓
[6] OpenAI → 음성 응답 → audio 재생

마무리

이 구조는 최신 브라우저 기술(WebRTC)과 OpenAI의 실시간 음성 모델을 활용한 고성능 음성 인터페이스를 구현하는 데 적합합니다. 마이크만 있으면, 어떤 플랫폼에서도 작동 가능한 구조이며, 차후 모바일 또는 데스크탑 앱으로의 확장도 용이합니다.


📚 참고 링크

profile
프론트마스터를 꿈꾸는...

0개의 댓글