
본편("브라우저 녹음은 되는데 음성합성 API는 왜 거절할까")에서 통합의 더 어려운 쪽은 변환이 아니라 응답이었다고 썼다. 이 글이 그 상세다. 변환은 우리가 통제하지만, 외부 API의 응답 형태는 우리가 정하지 못한다.
먼저 Content-Type을 믿었다
같은 음성합성 엔드포인트가 요청 옵션과 서버 사정에 따라 다른 모양으로 답을 줬다. 본문을 파싱하기 전에 Content-Type 헤더를 먼저 본다. 서버가 실제로 무엇을 보냈는지는 Content-Type이 가장 먼저 말해 준다.
JSON이면 base64 오디오와 음소 데이터를 객체로 받고, Content-Type이 audio 계열이면 본문을 바이너리로 받고 길이는 응답 헤더에서 읽는다. 둘 다 아닌데 JSON 안에 오디오 URL 필드가 있으면 URL 응답으로 처리한다. 셋 중 어디에도 안 맞으면 예상치 못한 응답 형식 오류로 명시적으로 끝낸다. 분기 순서는 서버의 진실(Content-Type)이 먼저고, 클라이언트의 기대는 그다음이다.
봉투 네 종과 별칭 일곱 종
음성 목록 조회는 더 복잡했다. 같은 의미의 데이터가 API 버전과 구성에 따라 네 가지 봉투로 왔다. 배열이 최상위로 오기도 하고, data와 meta로 싸여 오기도 하고, voices나 items라는 이름으로 오기도 했다. 어디에도 안 맞으면 응답 객체에서 첫 번째 배열 속성을 휴리스틱으로 집었다.
봉투를 벗기면 필드 이름이 또 제각각이었다. 식별자는 id, voice_id, uuid 중 하나로 오고, 이름은 name, title, display_name 중 하나로 온다. 이런 별칭 일곱 종을 하나의 표준 스키마로 빨아들이는 정규화 계층을 만들었다. 외부 API는 우리가 모르는 사이에 응답 구조를 바꾼다. 이 계층 하나가 API 버전이 흔들려도 화면이 깨지지 않게 하는 방파제였다.
상태코드를 사람 말로 번역한다
실패 상태코드는 사용자가 행동할 수 있는 안내로 매핑했다. 401은 인증 확인, 403은 요금제 안내, 413은 녹음 단축, 415는 허용 형식 안내, 429는 대기 후 재시도. 목표는 사용자가 상태코드의 의미를 몰라도 다음 행동을 알게 하는 것이다.
파일 업로드에도 작은 함정이 있었다. FormData 전송에서 Content-Type을 직접 지정하면 boundary 토큰이 빠져 서버가 요청을 제대로 읽지 못한다. 이때는 브라우저가 헤더를 자동으로 만들게 두는 편이 맞다.
정직하게 남은 비대칭
이 통합에는 아직 균형을 못 맞춘 지점이 있다. 음성복제 경로는 상태코드를 세밀하게 번역하는데, 음성 생성 경로는 상태코드와 응답 텍스트를 그대로 던진다. 같은 제품 안에서 한쪽은 친절하고 한쪽은 무뚝뚝하다. 다시 한다면 상태코드와 행동 안내의 매핑을 공통 계층으로 빼서 두 경로가 같은 친절함을 공유하게 만들 것이다.
429도 마찬가지다. 지금은 대기 후 재시도를 안내만 한다. 안내는 사용자에게 떠넘기는 것이고, 진짜 복원력은 클라이언트가 알아서 물러섰다 다시 시도하는 것이다.
정리
정규화 계층은 외부 API와 화면 코드 사이의 어긋남을 흡수한다. 응답 형태도, 스키마도, 실패 코드도 그 안에서 정리된다. 깨지면 사용자가 보고, 안 깨지면 아무도 모른다. 후자가 목표다. 통합의 품질은 잘될 때가 아니라 어긋날 때 드러난다.
다음에 읽을 글

긴 영상을 한 번에 렌더하지 않은 이유
서버리스 렌더에는 약 10분의 실행 시간 제한이 있다. 긴 영상은 프레임을 청크로 나눠 병렬로 렌더하고 하나로 결합했다. 총 시간은 가장 느린 청크 수준으로 줄었고, 이 병렬성은 렌더를 프레임 번호의 결정적 함수로 설계해 둔 데서 공짜로 얻었다.

수십 MB WASM을 한 번만 받게 만들기
수십 MB WASM 엔진은 언제 받느냐와 몇 번 받느냐가 체감을 가른다. 변환이 필요한 순간에만 받고, 싱글톤과 Promise 공유로 동시 요청에도 다운로드는 한 번으로 막았다. 실패하면 공유 상태를 비워 다음 시도가 처음부터 다시 시작하게 했다.

브라우저 녹음은 되는데 음성합성 API는 왜 거절할까
브라우저는 WebM·48kHz·스테레오로 녹음하고, 음성합성 API는 WAV·16kHz·Mono·3MB 이하 입력을 요구한다. WebAssembly 트랜스코더를 브라우저에서 필요할 때만 로드해 형식을 맞추고, 보내기 전에 용량을 두 번 검사했다. 기술보다 중요했던 건 둘 사이를 잇는 통합 계층이었다.