본문 바로가기

텍스트 음성 변환(TTS) API 연동: 스트리밍, 배치, 재시도

게시일

듣기이 기사 오디오로 듣기

텍스트 음성 변환(TTS) API 연동은 간단합니다. 다만, 몇 가지 중요한 결정을 먼저 해야 합니다. 어떤 전송 방식을 쓸지, 모델과 출력 포맷을 어떻게 고를지, 스트리밍 방식, 동시 처리 한도를 넘지 않고 대량 요청을 처리하는 방법, 캐싱과 재시도를 통해 같은 오디오에 중복 비용을 내지 않는 방법, 그리고 타사와의 첫 바이트 도달 시간(benchmarks)을 비교하는 방법 등입니다.

텍스트 음성 변환(TTS) API 연동을 돕기 위해, 이러한 아키텍처 결정 하나하나와 그에 따른 조치들을 모두 정리했습니다. 이 가이드를 통해 ElevenLabs 텍스트 음성 변환(TTS) API를 연동하고, 바로 적용할 수 있는 코드 예시로 빠르게 시작할 수 있습니다.

여기서 언급된 개념에 대한 자세한 배경은 아래 가이드에서 확인하세요: 오디오 스트리밍 이해하기, 지연 시간 최적화, 그리고 ElevenLabs 모델 개요.

요약

  • ElevenLabs 텍스트 음성 변환(TTS) API 엔드포인트는 하나이며, 배치 변환, HTTP 스트림, 스트림 입력 WebSocket 세 가지 방식으로 접근할 수 있습니다.
  • HTTP에서는 처리 중인 모든 요청이 동시 처리 한도에 포함되지만, WebSocket에서는 실제 오디오 생성 중인 요청만 한도에 포함됩니다.
  • 동시 처리 수를 요금제 한도보다 약간 낮게 설정하고, 오디오에 영향을 주는 모든 파라미터의 해시로 캐싱하면 같은 텍스트에 중복 비용이 발생하지 않습니다.
  • 429 및 5xx 에러는 지수 백오프와 풀 지터로 재시도하여 동시 처리 한도에 도달하기 전에 부하를 줄이세요.

텍스트 음성 변환(TTS) API 연동 방식 3가지

텍스트 음성 변환(TTS) 엔드포인트는 하나지만, 연동 방식에 따라 지연 시간, 복잡도, 비용이 달라집니다.

동일한 POST /v1/text-to-speech/{voice_id} 호출이 세 가지 방식으로 동작하며, 각각의 용도에 따라 적합합니다. 아래는 텍스트 음성 변환(TTS) API를 연동하는 세 가지 방법입니다:

  • 배치(변환)는 가장 간단한 연동 방식입니다: 한 번 요청하면 한 번 오디오 응답을 받습니다. 가장 단순하지만, 전체 오디오가 생성된 후에야 응답이 오기 때문에 첫 오디오까지 시간이 가장 오래 걸립니다.
  • HTTP 스트리밍(스트림)은 동일한 요청에 응답을 청크 단위로 나눠서 반환합니다: 경로에 /stream을 추가하고 스트림 메서드를 호출하면, 오디오가 청크로 반환됩니다. 코드는 거의 동일하며, 체감 지연 시간이 훨씬 짧아집니다.
  • WebSocket(스트림 입력)은 지속적인 연결을 유지합니다: 텍스트를 점진적으로 보내고, 오디오 청크를 실시간으로 받습니다. 대화형 에이전트나 LLM의 출력 토큰을 문장이 끝나기 전에 바로 음성으로 변환할 때 적합합니다.

스트리밍은 모델의 오디오 생성 속도를 빠르게 하진 않지만, 첫 청크를 전체 오디오가 완성되기 전에 보내주기 때문에 사용자가 느끼는 대기 시간이 짧아집니다.

배치 vs. 스트리밍 vs. WebSocket 결정표

이 세 가지 방식 중에서 선택할 때 고려해야 할 요소들이 있습니다.

간단히 정리하면: 오프라인 렌더링에는 배치, 사용자가 기다리는 텍스트에는 HTTP 스트리밍, 에이전트나 실시간 LLM 음성 변환에는 WebSocket을 선택하세요.

아래 표는 대규모 환경에서 중요한 요소별로 각 방식의 장단점을 정리한 것입니다.

Batch (convert)
Time-to-first-audio
Highest (wait for full clip)
Implementation complexity
Lowest
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Offline rendering, audiobooks, caching
HTTP streaming
Time-to-first-audio
Low
Implementation complexity
Low
Text known up front?
Required
Streaming LLM output into TTS
Awkward
Concurrency cost
Each request counts fully
Best for
Web/app playback of known text
WebSocket (stream-input)
Time-to-first-audio
Lowest
Implementation complexity
Highest (connection lifecycle, framing)
Text known up front?
Not required - send incrementally
Streaming LLM output into TTS
Native fit
Concurrency cost
Only active generation counts
Best for
Voice agents, live LLM to speech

HTTP(배치/스트리밍)에서는 처리 중인 모든 요청이 요금제의 동시 처리 한도에 포함됩니다. WebSocket에서는 모델이 실제로 오디오를 생성하는 시간만 한도에 포함되고, 연결만 열려 있고 대기 중일 때는 거의 비용이 들지 않습니다.

예를 들어, 연결을 유지하는 연속 대화형 음성 에이전트의 경우, 에이전트가 말할 때만 오디오를 생성하므로 이 차이가 큽니다. 이것이 에이전트 개발 시 WebSocket을 사용하는 주요 이유입니다. 전체 프로토콜은 실시간 텍스트 음성 변환 WebSocket 가이드에 문서화되어 있습니다.

모델 및 출력 포맷 선택하기

TTS API 연동 시 오디오 품질을 결정하는 두 가지 선택이 있습니다. 첫째, 모델(품질과 속도 결정), 둘째, 출력 포맷(컨테이너, 비트레이트, 샘플레이트 결정)입니다.

처음부터 이 두 가지를 잘 선택하면 이후의 지연 시간, 전화 호환성 등도 자연스럽게 맞춰집니다.

모델

여러 가지 텍스트 음성 변환 모델을 제공합니다. 각 모델은 용도에 따라 장단점이 다르며, 순위가 있는 것은 아닙니다.

Best for
eleven_flash_v2_5
Real-time, agents, bulk throughput (~75ms model inference)
eleven_flash_v2
Real-time, English only (~75ms)
eleven_multilingual_v2
Highest stable fidelity, narration
eleven_v3
Most expressive, widest language range
Languages
eleven_flash_v2_5
32
eleven_flash_v2
English
eleven_multilingual_v2
29
eleven_v3
70+
Character limit
eleven_flash_v2_5
40,000
eleven_flash_v2
30,000
eleven_multilingual_v2
10,000
eleven_v3
5,000

참고로, 약 75ms 수치는 네트워크 및 앱 지연 시간을 제외한 모델 추론 시간입니다. 입력이 길거나 부하가 많을수록 늘어날 수 있습니다. 항상 벤치마크 수치가 아닌 실제 앱에서 측정하세요.

Flash 모델은 더 작고, 추론 시간을 줄이기 위해 더 과감한 근사치를 사용합니다. Eleven v3와 Multilingual v2는 더 큰 모델로, 문자당 더 많은 연산을 하여 더 풍부한 출력을 만듭니다. Flash 속도에 Eleven v3 품질을 제공하는 설정은 없습니다. 품질이 곧 추가 연산이기 때문입니다.

실시간 또는 에이전트 용도에는 eleven_flash_v2_5를 사용하세요. 가장 낮은 지연 시간의 다국어 옵션입니다. 내레이션, 오디오북, 마케팅 보이스오버에는 안정적인 고음질이 필요할 때 eleven_multilingual_v2, 최대 표현력과 감정이 필요할 때 eleven_v3를 추천합니다.

전화번호, 날짜, 통화 등 발음이 중요한 경우에는 텍스트가 API에 도달하기 전에 앱에서 직접 숫자 정규화를 해주세요. 원하는 발음대로 텍스트를 작성하는 것이 좋습니다.

직접 정규화하면 모델별 기본값에 의존하지 않아도 되어, 모델이 바뀌어도 발음이 예측 가능해집니다.

출력 포맷

output_format 파라미터는 오디오의 컨테이너, 샘플레이트, 비트레이트를 결정합니다. 주로 사용하는 값은 다음과 같습니다:

Use case
mp3_44100_128
General playback, downloads, highest mp3 quality shown here
mp3_22050_32
Lower-bandwidth playback, smaller files
pcm_24000 / pcm_16000
Raw PCM for your own audio pipeline or further processing
ulaw_8000
Telephony - the format used with Twilio and similar systems
Languages
mp3_44100_128
32
mp3_22050_32
English
pcm_24000 / pcm_16000
29
ulaw_8000
70+
Character limit
mp3_44100_128
40,000
mp3_22050_32
30,000
pcm_24000 / pcm_16000
10,000
ulaw_8000
5,000

음성 설정

아래 설정들은 생성된 음성의 전달 방식을 제어합니다:

  • Stability: 일관성(안정성)과 표현력의 정도를 조절합니다. 낮게 설정하면 더 다양하고 표현력 있는 음성이, 높게 설정하면 더 안정적이고 예측 가능한 음성이 생성됩니다.
  • SimilarityBoost: 출력이 기준 음성과 얼마나 유사한지 조절합니다.
  • Style: 값을 높이면 음성의 자연스러운 말투가 더 강조됩니다.
  • useSpeakerBoost: 약간의 지연 시간 증가와 함께 원래 화자와의 유사성을 높입니다.
  • Speed: 기본값(1.0)을 기준으로 말하는 속도를 조절합니다.

이 중 Stability가 체감 품질에 가장 큰 영향을 줍니다. 낮게 설정하면 더 표현력 있지만 일관성은 떨어지고, 높게 설정하면 일관성과 예측 가능성이 우선됩니다.

음성 선택 시, 가장 낮은 지연 시간 조합은 Flash 모델과 인스턴트 음성 복제 또는 기본 음성입니다. 프로페셔널 음성 복제는 뛰어난 품질을 제공하지만, 생성마다 추가 시간이 소요됩니다.

이 가이드에서는 샘플 voice id로 JBFqnCBsd6RMkjVDRZzb(George)를 사용합니다.

스트리밍 연동(HTTP 및 WebSocket)

이 섹션에서는 텍스트 음성 변환(TTS) API 연동의 실전 핵심을 다룹니다. SDK 설치, 스트림 열기, 오디오 수신 과정을 안내합니다. HTTP 방식은 대부분의 웹/앱 재생에, WebSocket 방식은 에이전트 및 실시간 LLM 출력에 적합합니다.

두 방식 모두 아래와 같이 ElevenLabs 클라이언트가 초기화되어 있다고 가정합니다.

npm install @elevenlabs/elevenlabs-js
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

스트리밍 방식은 스트림을 열고, 도착하는 청크를 순차적으로 소비합니다. voiceId가 첫 번째 인자이며, 옵션 객체(modelId, outputFormat, voiceSettings)는 카멜케이스 키로 전달합니다:

const stream = await elevenlabs.textToSpeech.stream("JBFqnCBsd6RMkjVDRZzb", {
  text,
  modelId: "eleven_flash_v2_5",
  outputFormat: "mp3_44100_128",
  voiceSettings: { stability: 0, similarityBoost: 1.0, style: 0, useSpeakerBoost: true, speed: 1.0 },
});

for await (const chunk of stream) {
  // chunk is a Buffer; feed it to the player as it arrives
}

WebSocket 방식에서는 wss://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream-input에 연결하고, 음성 설정과 앞에 공백이 포함된 첫 메시지를 보낸 뒤, 텍스트 메시지를 순차적으로 보내고, 오디오가 base64로 인코딩된 JSON 프레임을 받아 읽습니다.

대량 처리용 배치 및 동시 처리 한도

대량 연동은 동시 처리(concurrency), 즉 동시에 오디오를 생성하는 요청 수에 의해 제한됩니다. 각 요금제마다 모델별 한도가 있습니다.

각 요금제별 동시 처리 한도는 다음과 같습니다:

  • 무료: Flash 요청 최대 4개 동시 처리.
  • 스타터: Flash 요청 최대 6개 동시 처리.
  • 크리에이터: Flash 요청 최대 10개 동시 처리.
  • 프로: Flash 요청 최대 20개 동시 처리.
  • 스케일 및 비즈니스: Flash 요청 최대 30개 동시 처리, 엔터프라이즈는 맞춤 한도 제공.

Multilingual v2 한도는 위의 절반 수준입니다.

바운디드 풀(bounded pool)을 사용하면 동시에 실행되는 요청 수를 제한할 수 있습니다:

// Set MAX_CONCURRENCY at or below your plan's Flash concurrency limit.
const MAX_CONCURRENCY = 8;

async function synthMany(texts: string[]): Promise<Buffer[]> {
  const results: Buffer[] = [];
  for (let i = 0; i < texts.length; i += MAX_CONCURRENCY) {
    const batch = texts.slice(i, i + MAX_CONCURRENCY);
    results.push(...(await Promise.all(batch.map(eachSingleRequest)))); // never more than MAX_CONCURRENCY in flight
  }
  return results;

MAX_CONCURRENCY를 요금제 한도보다 약간 낮게 설정하세요. 이 여유 공간이 동일 키를 공유하는 다른 트래픽을 흡수해 429 에러를 방지합니다.

문자 수 제한 및 긴 텍스트 분할

모든 모델은 한 번의 요청에 허용하는 문자 수가 정해져 있습니다. 긴 텍스트는 분할해서 오디오를 이어붙여야 합니다.

모델별 요청당 문자 수 한도는 다음과 같습니다:

  • Flash v2.5: 요청당 최대 40,000자까지 허용.
  • Flash v2: 요청당 최대 30,000자까지 허용.
  • Multilingual v2: 요청당 최대 10,000자까지 허용.
  • Eleven v3: 요청당 최대 5,000자까지 허용.

이보다 긴 텍스트는 여러 요청으로 분할해야 합니다. 문장 단위로 분할하면 청크 사이의 억양(프로소디)이 자연스럽게 이어집니다.

function splitText(text: string, maxChars: number): string[] {
  const sentences = text.trim().split(/(?<=[.!?])\s+/);
  const chunks: string[] = [];
  let current = "";
  for (let sentence of sentences) {
    if (current.length + sentence.length + 1 > maxChars) {
      if (current) chunks.push(current.trim());
      // A single sentence longer than the limit is hard-split.
      while (sentence.length > maxChars) {
        chunks.push(sentence.slice(0, maxChars));
        sentence = sentence.slice(maxChars);
      }
      current = sentence;
    } else {
      current = `${current} ${sentence}`.trim();
    }
  }
  if (current) chunks.push(current.trim());
  return chunks;
}

청크를 순서대로 렌더링하고 오디오를 이어붙이세요. 각 청크가 독립적인 장문 내레이션의 경우, splitText 결과를 위의 바운디드 풀에 넣어 처리하면 됩니다.

캐싱 및 멱등성(idempotency)

텍스트 음성 변환 결과는 동일한 텍스트, 음성, 모델, 설정으로 재생성하면 항상 같으므로, 중복 생성은 낭비입니다. 오디오에 영향을 주는 입력값의 해시로 결과를 캐싱하고, 이 키를 재시도 시 멱등성 토큰으로도 사용하세요.

방법은 다음과 같습니다.

import { createHash } from "node:crypto";

function cacheKey(text: string, voiceId: string, modelId: string,
                  outputFormat: string, settings: object): string {
  // Every parameter that changes the audio must be in the key.
  const payload = JSON.stringify({ text, voiceId, modelId, outputFormat, settings });
  return createHash("sha256").update(payload).digest("hex");
}

async function cachedSynth(text: string, voiceId: string, modelId: string,
                           outputFormat: string, settings: object): Promise<Buffer> {
  const key = cacheKey(text, voiceId, modelId, outputFormat, settings);
  const cached = await cacheGet(key);          // e.g. read from disk or S3
  if (cached) return cached;

  const audio = await elevenlabs.textToSpeech.convert(voiceId, { text, modelId, outputFormat });
  await cachePut(key, audio);                   // store the bytes under the key
  return audio;
}

이 방식의 핵심은 오디오에 영향을 주는 모든 파라미터(outputFormat, voice 설정 등)가 키에 포함되어야 한다는 점입니다. 제대로 구현하면 동일 키가 멱등성 토큰 역할도 합니다. 이미 성공한 요청을 클라이언트가 재시도하면, 새로 생성하지 않고 캐시된 바이트를 반환하면 됩니다.

에러 처리 및 속도 제한(429 에러)

운영 환경에서는 백오프와 지터를 적용한 재시도, 그리고 상태 코드별로 다른 처리가 필요합니다. 일부 실패는 재시도할 가치가 있고, 일부는 그렇지 않기 때문입니다.

아래 표는 각 상태 코드별 권장 조치를 정리한 것이며, 429가 완전한 차단이 아닌 '소프트 리밋'인 이유도 설명합니다.

Meaning
401
Authentication failed
422
Invalid request
429
Concurrency exceeded
5xx
Transient server error
Action
401
Do not retry. Check the xi-api-key header and key validity.
422
Do not retry. Fix the payload (bad voice id, unsupported format, text over limit).
429
Retry with exponential backoff and jitter.
5xx
Retry with backoff.
Character limit
401
40,000
422
30,000
429
10,000
5xx
5,000

429는 완전한 차단이 아니며, 동작 방식을 이해하면 도움이 됩니다. 동시 처리 한도를 초과하면 우선 우선순위에 따라 요청이 대기열에 들어가며, 보통 약 50ms가 추가됩니다. 그 이후에도 한도를 초과하면 429가 반환됩니다.

응답에는 현재 동시 요청 수와 최대 동시 요청 수 헤더가 포함되어 있어, 이를 참고해 한도에 도달하기 전에 부하를 줄일 수 있습니다.

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function synthWithRetry(text: string, voiceId: string, maxRetries = 5): Promise<Buffer> {
  let delay = 500; // ms, base for exponential backoff
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await elevenlabs.textToSpeech.convert(voiceId, {
        text, modelId: "eleven_flash_v2_5", outputFormat: "mp3_44100_128",
      });
    } catch (err: any) {
      const status = err.statusCode;
      // 401/422 and exhausted retries are not recoverable here.
      if (!RETRYABLE.has(status) || attempt === maxRetries) throw err;
      // Exponential backoff with full jitter.
      await new Promise((r) => setTimeout(r, Math.random() * delay));
      delay = Math.min(delay * 2, 8000);
    }
  }
  throw new Error("unreachable");
}

더 많은 여유가 필요하다면 재시도 로직 개선보다 요금제 업그레이드를 고려하세요. 엔터프라이즈 고객은 계정 매니저를 통해 한도 상향을 요청할 수 있습니다.

지연 시간 및 첫 바이트 도달 시간 벤치마킹

지연 시간은 지역, 입력, 현재 부하에 따라 달라지므로, 신뢰할 수 있는 수치는 직접 환경에서 측정한 값뿐입니다.

이 섹션에서는 Flash 스트리밍 엔드포인트의 첫 바이트 도달 시간(TTFB)을 안내하며, 동일한 방식으로 타사와 비교할 수 있도록 구성되어 있습니다.

이것은 결과가 아니라 방법론입니다. 단일 실행 결과는 보장하지 않습니다.

텍스트 음성 변환(TTS) API 연동의 지연 시간을 벤치마킹할 때 주의할 점은 다음과 같습니다:

  • 네트워크 왕복 시간 포함:TTFB는 지리적 위치와 제공자의 최근 클러스터에 따라 달라지므로, 실제 서버가 위치한 곳에서 테스트하세요.
  • 워밍업 실행은 버리기:콜드 커넥션에서의 첫 요청은 느릴 수 있으니, 결과에 포함하지 마세요.
  • 입력값 고정:입력 길이, 음성, 모델, 부하에 따라 결과가 달라지므로, 제공자별로 동일하게 맞추세요.
  • 분포로 결과 보고:실행마다 값이 다르므로, 단일 값 대신 중앙값(median)과 p95를 공개하세요.

이 사항들을 고려했다면 벤치마킹을 시작할 준비가 된 것입니다.

const TEXT = "This is a fixed benchmark sentence used for every provider.";

async function measureElevenLabs(): Promise<number> {
  const start = performance.now();
  const res = await fetch(
    "https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream?output_format=mp3_44100_128",
    {
      method: "POST",
      headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY!, "Content-Type": "application/json" },
      body: JSON.stringify({ text: TEXT, model_id: "eleven_flash_v2_5" }),
    },
  );
  for await (const _ of res.body!) {
    return performance.now() - start; // first chunk received
  }
  throw new Error("no audio returned");
}

타사와 비교하려면 동일한 형태의 함수를 작성하세요. 워밍업 호출 1회를 버리고, 약 20회 샘플을 시간 간격을 두고 측정한 뒤, 중앙값과 p95를 밀리초 단위로 기록하세요.

공정한 비교는 변수를 통제하는 데 달려 있습니다.

두 제공자를 동일한 머신과 네트워크(가능하다면 실제 배포 지역의 서버)에서 실행하세요. 입력 텍스트도 동일하게 하고, 오디오는 짧게 유지해 모델 추론 시간이 결과에 더 큰 영향을 주도록 하세요. 여러 번 실행해 중앙값과 p95를 기록하세요. 단일 측정치는 신뢰할 수 없습니다.

공용 인터넷에서의 TTFB는 네트워크 왕복 시간 20~200ms가 포함되어 모델과는 무관할 수 있습니다. ElevenLabs는 북미, 유럽, 동남아 클러스터에서 서비스하며, 가장 가까운 곳으로 라우팅하니, 테스트 클라이언트도 해당 지역에 맞춰야 데이터센터 거리만 측정하는 일이 없습니다.

텍스트 음성 변환(TTS) API 연동 핵심 요약

운영 환경에서의 텍스트 음성 변환(TTS) API 연동은 몇 가지 중요한 결정에 달려 있습니다.

이 부분만 잘 선택하면 나머지는 자연스럽게 해결됩니다:

  • 작업별 모델 선택: 실시간/인터랙티브 용도에는 Flash v2.5, 고음질이 필요한 오프라인 렌더링에는 Multilingual v2 또는 Eleven v3를 사용하세요.
  • 사용자가 기다릴 때는 스트리밍: 알려진 텍스트에는 HTTP 스트리밍, 에이전트에는 WebSocket을 사용해 대기 시간이 동시 처리 한도에 포함되지 않도록 하세요.
  • 동시 처리 수는 요금제 한도에 맞추기: 동시 요청 수를 한도보다 약간 낮게 제한하고, 오디오에 영향을 주는 모든 파라미터의 해시로 캐싱해 중복 과금이 없도록 하세요.
  • 429 및 5xx는 지수 백오프와 풀 지터로 재시도: 429 및 5xx 발생 시 풀 지터로 백오프하고, 동시 처리 헤더를 참고해 한도에 얼마나 가까운지 확인하세요.
  • 긴 텍스트는 문장 단위로 분할: 각 모델의 문자 수 한도 내에서 문장 단위로 분할해 억양이 자연스럽게 이어지도록 하세요.

더 깊이 배우고 싶다면 스트리밍 활용법, 오디오 스트리밍 개념, 인증, 그리고 클라이언트용 일회용 토큰을 참고하세요.

ElevenAPI로 텍스트 음성 변환 연동 시작하기

이 가이드를 모두 읽었다면, 운영 환경에서 텍스트 음성 변환(TTS) API 연동에 필요한 모든 패턴을 익힌 것입니다. 스트리밍, 배치, 캐싱, 재시도, 벤치마킹까지 모두 준비되었습니다.

아래에서 텍스트 음성 변환(TTS) API를 더 알아보거나, 회원가입 후 오늘 바로 ElevenAPI로 첫 호출을 시작해보세요.

텍스트 음성 변환(TTS) API 연동 FAQ

유사한 기사

최고 품질의 AI 오디오로 창작하세요