블로그로 돌아가기
2026년 10월 19일Sergei Solod27 분 읽기

Whisper와 WebAssembly로 웹사이트에 개인정보 보호형 음성 입력 구현하기

Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet, WebAssembly를 활용해 음성을 브라우저 내부에서만 텍스트로 변환하는 방법을 설명합니다. 모바일 성능 최적화와 음성 인식을 막는 런타임, 워커, 토크나이저 오류 해결 방법도 다룹니다.

음성 텍스트 변환WhisperWebAssemblyAudioWorkletJavaScript

웹사이트에 마이크 버튼을 추가하는 일은 간단해 보입니다. 사용자의 음성을 녹음하고 말을 인식한 뒤 입력란에 넣으면 끝일 것 같기 때문입니다. 하지만 데스크톱과 모바일 브라우저에서 안정적으로 작동하도록 만드는 일은 훨씬 어렵습니다. 특히 마이크 음성을 서버에 업로드하지 않고 음성 인식의 모든 과정을 기기 내부에서 처리해야 한다면 더욱 그렇습니다.

어려운 부분은 음성 인식 모델을 선택하는 데 그치지 않습니다. 실제로 작동하는 구현에는 마이크 권한, 오디오 캡처, 리샘플링, 브라우저 워커, WebAssembly, ONNX 모델 초기화, 토큰화, 캐싱, 애플리케이션의 텍스트 입력과의 연동까지 올바르게 처리하는 과정이 필요합니다.

이 글에서는 OpenAI Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet, 전용 Web Worker를 사용해 개인정보 보호에 중점을 둔 실용적인 음성 텍스트 입력 기능을 구축하는 방법을 설명합니다. 모델 파일 다운로드에는 성공했는데도 마이크 버튼이 로딩 상태에 멈춰 있는 등 실제 통합 과정에서 발생한 여러 오류도 다룹니다.

목표는 명확합니다. 방문자가 마이크 버튼을 누르고 자연스럽게 말하면 인식된 내용이 기존 입력란에 나타납니다. 사용자는 제출하기 전에 결과를 확인하거나 수정할 수 있습니다. 녹음된 음성 자체는 브라우저 밖으로 나갈 필요가 없습니다.

완전한 클라이언트 측 음성 인식이란 무엇인가

웹사이트에 음성 입력을 추가하는 방법은 근본적으로 두 가지입니다.

첫 번째는 음성 인식 서비스를 사용하는 방식입니다. 브라우저가 오디오를 캡처해 원격 서비스에 전송하고 인식된 텍스트를 돌려받습니다. 구현은 간단해질 수 있지만 네트워크 의존성, 개인정보 보호 문제, 서비스 제공 여부, 사용량에 따른 비용이 발생할 수 있습니다.

두 번째는 브라우저 안에서 직접 추론을 수행하는 방식입니다. 웹사이트가 음성 인식 모델과 이를 실행할 런타임을 다운로드합니다. 브라우저는 마이크 오디오를 캡처해 수치 샘플로 변환하고, 로컬에서 모델을 실행해 음성 인식 서버 없이 전사 결과를 생성합니다.

이 글에서는 두 번째 방식만 다룹니다.

일반적인 브라우저 음성 인식과 로컬 처리가 보장되는 음성 인식을 혼동해서는 안 됩니다. Web Speech API는 서버 기반 인식 엔진을 사용할 수 있습니다. 일부 최신 구현에는 processLocally 옵션이 있지만 브라우저와 운영체제마다 지원 수준이 다릅니다. 엄격한 로컬 처리가 필요하다면 일반적인 SpeechRecognition을 자동 대체 수단으로 사용하면 안 됩니다.

대신 애플리케이션이 전체 추론 파이프라인을 직접 제어해야 합니다.

Microphone
    |
    v
AudioWorklet
    |
    v
Float32 PCM audio
    |
    v
Dedicated Web Worker
    |
    v
16 kHz resampling
    |
    v
Whisper + ONNX Runtime Web
    |
    v
Tokenizer and text decoding
    |
    v
React input state

웹사이트는 여전히 JavaScript, WebAssembly, 설정 파일, 모델 가중치를 제공합니다. 이들은 일반적인 정적 파일 다운로드입니다. 핵심적인 차이는 인식을 위해 마이크 오디오를 업로드하는 대신 로컬에서 처리한다는 점입니다. 이후 사용자가 일반적인 양식으로 인식된 텍스트를 제출하는 것은 별도의 애플리케이션 동작입니다.

실용적인 모델 선택: Whisper Tiny와 Base

Whisper는 다국어 음성 인식 모델 계열입니다. 하나의 모델로 여러 언어를 인식할 수 있으므로 다국어 웹사이트에 특히 적합합니다.

짧은 음성 메시지에서는 Whisper Tiny와 Whisper Base가 인식 품질과 브라우저 자원 사용량 사이에서 유용한 균형을 제공합니다.

모델선택한 Q8 ONNX 가중치의 대략적인 크기권장 용도
Whisper Tiny41 MB모바일 및 자원이 부족한 환경에서의 보수적인 선택
Whisper Base77 MB성능이 검증된 기기에서 더 높은 품질을 위한 선택

이 크기는 조사한 내보내기 파일 중 선택한 인코더와 디코더의 ONNX 파일을 기준으로 합니다. 모든 보조 파일을 포함하지 않으며 브라우저의 최대 메모리 사용량과 혼동해서는 안 됩니다.

Whisper Tiny는 다운로드가 빠르고 보통 연산 자원이 적게 들며 성능을 알기 어려운 모바일 환경에 더 적합합니다. Whisper Base는 일반적으로 인식 품질이 더 좋으며, 특히 작은 모델에서 오류가 더 많이 발생하는 일부 언어에서 유리합니다.

물리적인 RAM 용량만 보고 모델을 선택해서는 안 됩니다. 브라우저가 실제로 사용할 수 있는 메모리는 기기의 사양에서 예상하는 것보다 훨씬 적을 수 있습니다. 이미 열린 페이지가 사용하는 메모리, WebAssembly 메모리 할당, 임시 텐서, 운영체제의 메모리 압박도 모두 고려해야 합니다.

보수적으로 접근한다면 성능을 알 수 없는 모바일 기기에서는 Tiny로 시작하고, 기준을 충족하는 기기에만 Base를 사용하며, Base 초기화에 실패하면 Tiny로 전환하는 편이 좋습니다.

Base 인스턴스가 실패했다면 Tiny를 불러오기 전에 해제해야 합니다. 두 모델을 모두 메모리에 남겨 두면 대체 모델을 사용하는 목적이 사라집니다.

WebAssembly가 프로덕션 환경의 합리적인 기본값인 이유

ONNX Runtime Web은 브라우저 안에서 신경망을 실행할 수 있습니다. 이 작업에서는 WebAssembly가 주요 데스크톱 및 모바일 브라우저에서 실용적인 CPU 추론 경로를 제공합니다.

WebGPU는 적합한 하드웨어에서 지원되는 모델을 가속할 수 있지만, 사용할 수 있다는 사실이 음성 인식 파이프라인의 정상 작동을 보장하지는 않습니다. 브라우저에 navigator.gpu가 있더라도 ONNX Runtime이 해당 플랫폼을 지원하는지, 특정 Whisper 그래프가 올바르게 초기화되는지, 반복 추론에서 자원 문제가 발생하지 않는지는 보장되지 않습니다.

따라서 신중한 구현이라면 프로덕션 백엔드로 WASM을 사용하고, WebGPU는 호환성·정확도·메모리·반복 세션 테스트를 거친 뒤 별도로 평가할 수 있습니다.

초기 구현에서 단일 스레드 WASM을 사용하면 웹사이트 전체를 교차 출처 격리와 SharedArrayBuffer에 의존하도록 만들 필요도 없습니다.

env.backends.onnx.wasm.numThreads = 1;
env.backends.onnx.wasm.proxy = false;

전용 워커 안에서 추론을 실행하면 무거운 연산이 JavaScript 메인 스레드를 차지하지 않습니다. 모델이 음성을 처리하는 동안에도 입력란, 가상 키보드, 스크롤, 애플리케이션 UI가 응답성을 유지할 수 있습니다.

초기 페이지 로딩에서 음성 인식을 완전히 제외하기

마이크 기능을 사용하지 않는 방문자에게 큰 네트워크 비용과 메모리 부담을 지워서는 안 됩니다.

초기 애플리케이션 번들에 Transformers.js를 미리 가져오지 마세요. 페이지를 열 때 ONNX Runtime을 초기화하거나 유휴 시간에 모델 가중치를 자동으로 다운로드하는 것도 피해야 합니다.

사용자가 명시적으로 마이크 버튼을 클릭한 뒤에만 인식에 필요한 구성요소를 불러오세요.

처음 사용할 때는 다음 순서가 유용합니다.

  1. 마이크 버튼 클릭을 받습니다.
  2. getUserMedia()를 통해 마이크 권한을 요청합니다.
  3. 권한이 거부되면 모델을 다운로드하지 않고 오류를 표시합니다.
  4. 권한이 허용되면 모델을 준비하는 동안 임시 마이크 스트림을 해제합니다.
  5. 전용 워커를 동적으로 초기화하고 선택한 로컬 모델을 불러옵니다.
  6. 워커가 인식 파이프라인 준비 완료를 알릴 때까지 기다립니다.
  7. 사용자가 취소하지 않았다면 자동으로 녹음을 시작합니다.
  8. 녹음 중임을 이해할 수 있는 상태 표시를 제공합니다.

권한 요청은 사용자의 동작과 연결되어야 합니다. 언제나 별도 조작 없이 녹음을 다시 시작할 수 있다고 가정하지 말고 취소된 작업과 브라우저별 캡처 제한도 처리해야 합니다.

모델을 준비하는 동안에는 음성 입력 준비 중과 같이 알아볼 수 있는 상태를 표시해야 합니다. 의미 있는 상태 변화 없이 로딩 표시만 계속되면 모델 다운로드가 성공했더라도 마이크가 고장 난 것처럼 보일 수 있습니다.

초기화된 워커는 다음 받아쓰기를 위해 잠시 유지할 수 있습니다. 약 1분 동안 유휴 상태가 이어지면 종료하는 설정은 합리적인 시작점입니다. 이후 워커를 종료하고 메모리에 올라간 모델을 해제합니다. 브라우저에 캐시된 모델 파일은 다음 방문에도 사용할 수 있습니다.

AudioWorklet으로 원시 오디오 캡처하기

Whisper에는 수치화된 오디오 샘플이 필요합니다. MediaRecorder는 오디오 파일 녹음에 매우 적합하지만 로컬 음성 인식에 사용하면 불필요한 인코딩과 디코딩 과정이 생길 수 있습니다.

더 간단한 처리 경로는 다음과 같습니다.

getUserMedia()
    |
    v
AudioContext
    |
    v
AudioWorklet
    |
    v
Float32 PCM buffers
    |
    v
Dedicated Worker

AudioWorklet은 애플리케이션의 JavaScript 메인 스레드 밖에서 오디오 처리 코드를 실행합니다. PCM 샘플을 캡처하고, 여러 채널을 모노로 줄이고, 간단한 음량을 측정하고, 누적된 버퍼를 워커로 전달할 수 있습니다.

마이크 초기화를 단순화한 예시는 다음과 같습니다.

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    channelCount: { ideal: 1 },
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
});

const context = new AudioContext();

await context.audioWorklet.addModule(
  '/assets/speech/pcm-worklet.js'
);

const source = context.createMediaStreamSource(stream);
const processor = new AudioWorkletNode(
  context,
  'pcm-capture'
);

source.connect(processor);

// Connect the worklet through an inaudible output path
// if required to keep the audio processing graph active.

마이크 제약 조건은 선호 설정이지 보장 사항이 아닙니다. 브라우저마다 에코 제거, 소음 억제, 자동 이득 제어 알고리즘을 다르게 적용할 수 있습니다.

중요한 점은 마이크의 샘플링 레이트가 반드시 16 kHz인 것은 아니라는 사실입니다. 많은 오디오 컨텍스트는 44.1 kHz나 48 kHz를 사용합니다. Whisper는 16 kHz 입력을 기대하므로 캡처한 샘플을 올바르게 리샘플링해야 합니다.

두세 개마다 샘플 하나를 단순히 버리는 방식은 일반적으로 올바른 해결책이 아닙니다. 윈도 함수가 적용된 sinc 방식처럼 적절한 저역 통과 리샘플러를 사용하면 샘플링 레이트를 낮출 때 에일리어싱을 줄이면서 음성에 필요한 주파수 성분을 유지할 수 있습니다.

리샘플링 작업은 실시간 AudioWorklet 콜백이 아니라 전용 워커에서 처리하세요.

브라우저 스레드 사이에서 오디오를 효율적으로 전송하기

AudioWorklet은 작은 오디오 조각을 많이 만들 수 있습니다. 오디오 렌더링의 모든 처리 단위를 개별 메시지로 보내면 불필요한 오버헤드가 발생할 수 있습니다.

실용적인 방법은 PCM을 짧은 묶음으로 누적한 뒤 각 묶음을 ArrayBuffer로 전달하는 것입니다.

worker.postMessage(
  {
    type: 'audio-chunk',
    buffer,
  },
  [buffer]
);

전송 목록이 중요한 이유는 ArrayBuffer 전체를 불필요하게 복사하지 않고 소유권을 넘길 수 있기 때문입니다.

워커는 이 조각들을 오디오 누적 버퍼에 추가했다가 녹음이 끝나면 합칠 수 있습니다.

실제 캡처 샘플링 레이트, 수집된 샘플 수, 녹음 길이를 기록하는 것도 유용합니다. 이러한 메타데이터로 민감한 오디오 데이터를 로그에 남기지 않고도 무음 녹음을 진단할 수 있습니다.

별도의 신경망을 추가하지 않고 발화 사이의 침묵 감지하기

짧은 메시지를 받아쓰는 데 반드시 신경망 기반 음성 활동 감지기가 필요한 것은 아닙니다.

적응형 RMS 감지기를 사용하면 발화 활동을 가볍게 추정할 수 있습니다. RMS는 신호의 진폭을 측정하므로 말하던 사람이 조용해진 시점을 대략적으로 판단하는 데 도움이 됩니다.

마이크 버튼을 다시 클릭하여 수동으로 녹음을 멈추는 기능은 항상 지원해야 합니다. 자동 침묵 감지는 편의 기능이지 성공적인 인식을 위한 필수 조건은 아닙니다.

짧은 받아쓰기를 위한 합리적인 초기 설정값은 다음과 같습니다.

항목시작값
분석 간격20 ms
소음 보정300 ms
발화 확인140 ms
자동 종료 전 침묵 시간1.4초
발화 없음 시간 초과8초
최대 녹음 길이30초

이 값들은 설정의 시작점일 뿐 모든 환경에 최적인 임계값은 아닙니다. 작은 목소리, 배경 음악, 마이크의 소음 처리 방식이 RMS 측정에 영향을 줄 수 있습니다.

문장 마지막의 작은 목소리를 자동으로 잘라 버리기보다는 사용자가 직접 녹음을 멈출 수 있게 하는 편이 낫습니다.

일반적인 짧은 텍스트 입력에서는 중간 결과를 얻기 위해 Whisper를 반복 실행하기보다 한 번에 전사하는 편이 대체로 단순합니다. 발화를 녹음하고 멈춘 뒤 모델을 한 번 실행하여 최종 전사 결과를 삽입합니다.

완전한 Whisper 파이프라인에는 ONNX 모델만으로는 부족하다

이는 구현에서 특히 중요한 부분 중 하나입니다.

Whisper 추론은 단순히 ONNX 인코더를 호출한 다음 ONNX 디코더를 호출하는 과정이 아닙니다. 완전한 자동 음성 인식 파이프라인에는 오디오 특징 추출, 토큰화, 생성 설정, 예측된 토큰을 읽을 수 있는 텍스트로 디코딩하는 과정도 필요합니다.

전체 처리 흐름은 다음과 같습니다.

Raw microphone PCM
        |
        v
Resampling to 16 kHz
        |
        v
Whisper feature extractor
        |
        v
ONNX encoder
        |
        v
ONNX decoder and token generation
        |
        v
Whisper tokenizer
        |
        v
Recognized text

ONNX 세션의 초기화 성공은 모델 그래프를 불러올 수 있다는 것만 증명합니다. 전체 인식 파이프라인의 정상 작동을 증명하는 것은 아닙니다.

테스트한 Transformers.js 통합 사례 중 하나에서는 모델 세션을 초기화할 수 있었지만, 로컬에서 구성한 파이프라인에 필요한 프로세서와 토크나이저가 올바르게 설정되지 않아 전사에 실패했습니다.

호환되는 구현에서는 로컬 모델 메타데이터를 사용해 이 구성요소들을 명시적으로 초기화했습니다.

다음 코드는 특정 버전에서 사용한 방법을 축약한 예시이며, 독립적으로 작동하는 완전한 워커는 아닙니다.

import {
  WhisperTokenizer,
  WhisperFeatureExtractor,
  WhisperProcessor,
} from '@huggingface/transformers';

// These values come from local model configuration files.
const tokenizer = new WhisperTokenizer(
  tokenizerJson,
  tokenizerConfig
);

const featureExtractor = new WhisperFeatureExtractor(
  preprocessorConfig
);

const processor = new WhisperProcessor(
  {},
  {
    tokenizer,
    feature_extractor: featureExtractor,
  },
  ''
);

// In the affected version-specific integration:
recognizer.tokenizer = tokenizer;
recognizer.processor = processor;

정확한 생성자 시그니처와 연동 방법은 설치된 Transformers.js 버전에 맞춰 확인해야 합니다. 일부 버전과 모델 내보내기에서는 해당 구성요소를 자동으로 생성할 수 있으므로 수동 생성이 항상 필요하다고 생각해서는 안 됩니다.

완전한 파이프라인이 준비되면 언어와 작업을 명시하여 짧은 녹음의 전사를 실행할 수 있습니다.

const result = await recognizer(audio16k, {
  language: 'en',
  task: 'transcribe',
  return_timestamps: false,
});

애플리케이션이 이미 사용자의 언어를 알고 있다면 이를 명시해 불필요한 언어 감지를 피하고 짧은 녹음에서 결과를 더 예측 가능하게 만들 수 있습니다.

호환되는 Transformers.js와 ONNX Runtime 버전 고정하기

두 번째 주요 오류 원인은 Transformers.js와 ONNX Runtime Web의 관계에 있습니다.

Transformers.js는 상위 수준의 인식 파이프라인을 제공하고 ONNX Runtime Web은 신경망 그래프를 실행합니다. 두 패키지는 서로 호환되어야 하고, 실제 사용하는 ONNX 모델 내보내기 파일과도 맞아야 합니다.

조사한 구현에서는 ONNX Runtime Web의 개발 빌드가 Whisper 인코더는 초기화했지만 양자화된 디코더 세션을 생성하는 데 실패했습니다. 이 오류는 DequantizeLinear 연산에 필요한 양자화 스케일이 누락된 것과 관련이 있었습니다.

이는 마이크 권한, 다운로드, 오디오 형식의 문제가 아니었습니다. 브라우저는 모델 파일을 받을 수 있었지만 런타임이 필요한 모델 그래프 중 하나를 초기화하지 못했습니다.

버전을 고정한 안정적인 ONNX Runtime Web 릴리스에서 인코더와 디코더를 직접 테스트하자 이 특정 호환성 문제를 해결할 수 있었습니다.

이 호환성 검사가 특히 유용했던 이유는 다음 두 가지 주장을 구분할 수 있었기 때문입니다.

  • 모델 파일을 성공적으로 다운로드했다.
  • 런타임이 모델 파일로부터 사용 가능한 추론 세션을 성공적으로 만들었다.

두 가지는 같은 뜻이 아닙니다.

테스트한 모델 내보내기에서는 Transformers.js 4.2.0과 ONNX Runtime Web 1.24.3의 조합이 호환되었습니다. 더 새로운 개발 버전의 런타임은 양자화 디코더 그래프에서 실패했습니다. 이는 특정 버전에 대한 관찰 결과이며, 오래된 런타임이 언제나 더 낫다는 증거는 아닙니다.

간접 의존성인 런타임 버전을 재정의한다면 패키지 잠금 파일에 정확한 버전을 고정하고 변경 이유를 기록하세요. 런타임 업그레이드 시에는 패키지 설치 성공 여부만이 아니라 브라우저 추론의 전체 회귀 테스트를 요구해야 합니다.

Whisper 디버깅 전에 워커 번들 형식부터 확인하기

JavaScript Web Worker는 서로 다른 모듈 형식으로 만들 수 있습니다.

통합 과정에서 발생한 오류 중 하나는 애플리케이션이 워커를 ES 모듈로 실행했지만 생성된 워커 번들이 클래식 워커 방식인 importScripts()를 사용했다는 점이었습니다.

두 실행 방식은 서로 바꿔 쓸 수 없습니다. 모듈 워커에서 클래식 워커 전용 코드를 그대로 실행할 수는 없습니다.

중요한 것은 일반적인 튜토리얼의 생성자 옵션을 복사하지 말고 실제 생성된 번들 형식에 맞게 Worker 생성자를 선택하는 것입니다.

예를 들어 다음 두 방식이 자동으로 동일하게 동작하는 것은 아닙니다.

// Module worker: requires a compatible module bundle.
new Worker(workerUrl, { type: 'module' });

// Classic worker: requires a compatible classic bundle.
new Worker(workerUrl);

올바른 선택은 번들러와 생성된 JavaScript에 따라 달라집니다.

워커 초기화 오류를 진단할 때는 브라우저의 Console, 생성된 워커 파일, Network 패널을 확인하세요. ONNX Runtime이 초기화를 시작하기도 전에 실패하는지부터 구분해야 합니다.

일부 번들된 브라우저 라이브러리는 자체 모듈 위치를 기준으로 URL을 계산하기도 합니다. 워커에 포함할 때는 원래 패키지의 상대 URL이 여전히 올바르게 해석된다고 가정하지 말고 실제 런타임 자산 경로를 테스트하세요.

WASM과 JavaScript 모듈을 올바른 MIME 유형으로 제공하기

JavaScript 및 ONNX 모델 파일이 완벽하게 호환되더라도 웹 서버가 잘못된 응답 헤더를 반환하면 실패할 수 있습니다.

실제 오류 사례에서는 확장자가 .mjs인 JavaScript 모듈이 application/octet-stream으로 제공되었습니다. 응답의 유형이 JavaScript 모듈 실행에 적합하지 않아 브라우저가 해당 모듈을 거부했습니다.

일반적으로 기대하는 응답 유형은 다음과 같습니다.

파일 확장자Content-Type
.mjsapplication/javascript
.wasmapplication/wasm

웹 서버 설정 파일만 보지 말고 실제 프로덕션 응답을 확인하세요.

curl -I https://example.com/runtime/speech/v2/runtime.mjs
curl -I https://example.com/runtime/speech/v2/runtime.wasm

위 URL은 설명을 위한 예시입니다. 선택한 런타임 빌드의 실제 버전별 자산 파일명을 사용하세요.

HTTP 상태 코드가 정상이라고 해서 충분한 것은 아닙니다. MIME 유형, Content Security Policy, 모듈 실행 방식, 잘못된 자산 URL 때문에 브라우저가 파일을 거부할 수 있습니다.

같은 요청이라도 브라우저 탭에서 직접 열면 성공하지만 실행 가능한 모듈이나 WebAssembly 의존성으로 불러오면 실패할 수 있다는 점을 기억하세요.

변경 불가능한 자산 URL을 사용하되 손상된 런타임을 캐시에 가두지 않기

Whisper 모델 파일은 용량이 크기 때문에 브라우저 캐싱이 중요합니다.

버전이 붙은 불변 자산 경로를 사용하면 사용자가 말할 때마다 변경되지 않은 모델과 런타임 파일을 다시 다운로드할 필요가 없습니다.

일반적인 캐시 정책은 다음과 같습니다.

Cache-Control: public, max-age=31536000, immutable

하지만 이 방식에는 배포 시 지켜야 할 조건이 있습니다. 한 번 URL을 불변으로 선언했다면 해당 URL의 내용을 몰래 교체해서는 안 됩니다.

런타임 바이너리를 바꿔야 한다면 새 버전의 URL로 배포하세요.

/models/speech/v1/
/runtime/speech/v1/
/runtime/speech/v2/

런타임 경로를 바꾸는 것은 단순한 이름 변경이 아닙니다. 불변 URL의 콘텐츠를 바꾸면서 같은 URL을 재사용하면 브라우저가 이전에 캐시한 호환되지 않는 WebAssembly 바이너리를 계속 사용할 수 있습니다.

JavaScript 연결 코드와 WebAssembly 파일도 같은 런타임 배포본에서 가져오는 것이 좋습니다. 서로 다른 런타임 버전의 파일을 섞으면 원인 파악이 어려운 초기화 오류가 발생할 수 있습니다.

예상 파일 목록, 크기, 암호학적 체크섬이 담긴 빌드 시점 매니페스트를 사용하면 배포 전에 누락되거나 일치하지 않는 자산을 찾아낼 수 있습니다.

인식 라이브러리가 자체 호스팅 자산을 사용하도록 설정하고 원격 모델 로딩을 비활성화하세요. 그러면 로컬 모델이 없을 때 외부 모델 호스트에서 뜻하지 않게 다운로드하는 대신 명확한 오류가 발생합니다.

아무것도 작동하지 않는데 마이크만 계속 로딩되는 이유

개발 중 자주 나타나는 증상은 마이크 버튼이 권한을 요청한 뒤 로딩 애니메이션을 표시하다가, 텍스트는 입력하지 못한 채 결국 오류 상태로 돌아가는 것입니다.

브라우저에 마이크를 사용 중이라는 표시가 나타날 수도 있습니다.

그렇다고 음성 인식기가 동작한다고 증명되는 것은 아닙니다. 어떤 시점에 마이크 캡처가 허용되었거나 활성화되었다는 사실만 알 수 있습니다.

서로 독립적인 여러 오류가 거의 똑같은 증상을 만들 수 있습니다.

  1. 잘못된 JavaScript 모듈 MIME 유형. 브라우저가 런타임 모듈을 다운로드하지만 실행은 거부합니다.
  2. 워커 실행 형식 불일치. 컴파일된 워커는 클래식 워커 API를 사용하는데 모듈 워커로 시작하거나, 그 반대인 경우입니다.
  3. ONNX 디코더 호환성 문제. 런타임이 인코더 세션은 생성하지만 양자화된 디코더 그래프에서 실패합니다.
  4. 불완전한 인식 파이프라인. ONNX 세션은 존재하지만 토크나이저나 오디오 프로세서가 올바르게 초기화되지 않았습니다.

이 문제들은 각각 독립적으로 진단해야 합니다. 하나를 고치면 다음 오류가 드러날 수도 있습니다.

디버깅에서 가장 중요한 원칙은 HTTP 200이 모델 초기화 성공을 뜻하지 않고, 모델 초기화 성공도 전사 성공을 뜻하지 않는다는 것입니다.

전사에 성공했더라도 결과가 애플리케이션의 실제 입력란까지 전달되었다는 보장은 없습니다.

각 처리 단계의 경계마다 의미 있는 진단 신호가 필요합니다.

개인 음성을 수집하지 않고 관측 가능성 확보하기

유용한 진단 로그를 위해 사용자가 한 말을 기록할 필요는 없습니다.

애플리케이션은 다음과 같은 기술적 상태를 추적할 수 있습니다.

permission-granted
worker-created
runtime-loading
runtime-ready
encoder-ready
decoder-ready
recording-started
pcm-received
recording-stopped
inference-started
inference-completed
result-inserted

실패할 때는 해당 단계, 일관된 기술 오류 코드, 민감한 정보를 제거한 오류 메타데이터를 포함하세요.

모델 초기화 실패와 마이크 권한 거부, 빈 녹음을 구분할 수 있어야 합니다. 워커 오류도 일반 오류 처리기에 묻히지 않고 개발 환경에서 확인할 수 있어야 합니다.

마이크 샘플, 인식된 문장, 토큰 시퀀스 또는 다른 음성 콘텐츠를 로그에 남기지 마세요. 성능 텔레메트리에는 시간 구간, 선택한 모델 등급, 초기화 시간, 추론 시간, 오류 범주 등을 사용할 수 있습니다.

이렇게 하면 개인정보 보호 요구사항을 약화시키지 않으면서 작동하지 않는 마이크를 훨씬 쉽게 진단할 수 있습니다.

React 상태를 통해 전사 결과 삽입하기

음성 인식은 최종 전사 결과가 기존 양식의 입력란에 들어가야 비로소 유용합니다.

React 애플리케이션에서 이런 입력란은 컴포넌트 상태로 제어되는 경우가 많습니다. input.value를 직접 설정하면 프레임워크의 렌더링 동작과 충돌할 수 있습니다.

따라서 인식된 텍스트는 사용자가 직접 입력할 때와 동일한 상태 업데이트 경로를 거쳐야 합니다.

연동 시 비동기 동작도 고려해야 합니다. 음성 인식이 끝나기 전에 사용자가 글자를 더 입력하거나 커서를 옮겼을 수 있습니다.

녹음을 시작할 때 저장한 오래된 입력값을 사용하면 그사이 이루어진 편집을 실수로 덮어쓸 수 있습니다.

간소화한 연동 예시는 다음과 같습니다.

const insertion = insertTranscript({
  currentValue: latestValue,
  transcript: recognizedText,
  selection: currentSelection,
  maxLength: inputLimit,
});

setMessage(insertion.value);
restoreCaret(insertion.caret);

텍스트 삽입 도우미는 현재 텍스트를 유지하고, 필요하면 선택된 영역을 교체하고, 적절한 공백을 처리하며, 입력란의 최대 길이를 지켜야 합니다.

Unicode도 주의해야 합니다. JavaScript의 문자열 길이는 UTF-16 코드 단위로 계산됩니다. 임의 위치에서 문자열을 자르면 서로게이트 쌍이 분리될 수 있습니다.

무엇보다 음성 인식이 양식을 자동으로 제출해서는 안 됩니다. 사용자가 전송하기 전에 텍스트를 확인하고 수정할 수 있어야 합니다.

일반적인 텍스트 입력을 망가뜨리지 않고 오류 처리하기

음성 기능은 선택적으로 사용할 수 있는 부가 기능이어야 합니다.

마이크 권한이 거부되거나 브라우저가 모델 메모리를 할당하지 못하거나, 워커가 중단되거나, 녹음에 음성이 없더라도 기존 텍스트 입력란은 계속 작동해야 합니다.

마이크의 생명주기는 다음과 같은 명시적인 상태로 표현합니다.

idle
preparing
listening
recognizing
error

접근성을 고려한 레이블과 눈에 보이는 상태 안내를 제공하세요. 로딩 후 마이크 색상만 빨강이나 분홍색으로 바뀌면 사용자에게 전달되는 정보가 거의 없습니다.

취소할 때는 오디오 트랙, 오디오 컨텍스트, 대기 중인 버퍼, 진행 중인 녹음 세션을 정리해야 합니다. 또한 이전에 취소한 녹음의 뒤늦은 결과가 새로운 입력 상태를 덮어쓰지 않도록 해야 합니다.

큰 모델이 메모리나 초기화 제약으로 실패했다면 작은 모델을 시도하기 전에 완전히 해제하세요. 실행할 수 없는 모델을 브라우저가 반복해서 다운로드하거나 초기화하지 않도록 재시도 횟수도 제한해야 합니다.

로컬 인식이 실패했다고 클라우드 음성 서비스로 조용히 전환해서는 안 됩니다. 로컬 전용 처리가 엄격한 요구사항이라면 로컬 인식 실패도 로컬에서 오류로 처리해야 합니다.

브라우저 파이프라인 전체 테스트하기

TypeScript 검사나 프로덕션 빌드에 성공했다고 음성 입력이 작동한다는 증거가 되지는 않습니다.

마찬가지로 브라우저가 ONNX 파일을 다운로드할 수 있다는 사실만으로 두 모델 세션이 모두 정상 초기화된다는 보장은 없습니다.

가장 유용한 회귀 테스트는 프로덕션과 동일한 워커, 런타임 파일, 모델, 프로세서, 토크나이저를 통해 실행되어야 합니다.

내용을 알고 있는 짧은 PCM 녹음을 실제 브라우저 추론 경로에 입력하고 비어 있지 않은 전사 결과가 반환되는지 확인하세요.

그다음 실제 브라우저에서 마이크 사용 과정 전체를 테스트하세요.

  • 초기 페이지 로딩: 마이크를 사용하기 전에 음성 모델이나 런타임 자산을 다운로드하지 않는지 확인합니다.
  • 콜드 초기화: 마이크 권한, 모델 요청, JavaScript 및 WASM의 MIME 유형, 실제 워커 준비 완료 응답을 확인합니다.
  • ONNX 초기화: 인코더와 디코더 세션을 모두 생성할 수 있는지 확인합니다.
  • 오디오 캡처: AudioWorklet이 비어 있지 않은 PCM 버퍼를 만드는지 확인합니다.
  • 리샘플링: 워커가 실제 샘플링 레이트를 받고 유효한 16 kHz 오디오를 만드는지 확인합니다.
  • 수동 중지: 마이크 버튼을 두 번째로 클릭했을 때 확실하게 녹음이 멈추는지 확인합니다.
  • 자동 중지: 자연스러운 침묵이 조용한 발화를 잘라내지 않으면서 녹음을 종료하는지 확인합니다.
  • 전사: 완전한 프로세서, 모델, 토크나이저를 통해 인식 가능한 텍스트가 반환되는지 확인합니다.
  • 입력란 연동: 인식된 텍스트가 실제 상태 제어 입력란에 들어가고 사용자의 편집 내용을 덮어쓰지 않는지 확인합니다.
  • 오류 복구: 권한 거부, 누락 파일, 워커 오류, 모델 초기화 실패, 취소, 작은 모델로의 대체를 테스트합니다.
  • 개인정보 보호: 네트워크 요청을 살펴보고 마이크 오디오가 서버나 분석 서비스에 업로드되지 않는지 확인합니다.
  • 모바일 호환성: 실제 휴대전화에서 콜드 시작, 웜 시작, 반복 받아쓰기 세션을 테스트합니다.

녹음된 PCM을 사용한 테스트는 추론 구성요소를 검증하는 데 매우 유용하지만 실제 마이크 캡처가 작동한다는 증거는 아닙니다. 최종 인수 테스트에는 실제 마이크와 실제 사용자 인터페이스가 포함되어야 합니다.

안정적인 브라우저 음성 입력을 위한 실용적인 아키텍처

짧은 다국어 음성 입력과 개인정보 보호가 필요한 웹사이트라면 의도적으로 단순하게 구성한 아키텍처가 가장 실용적입니다.

React microphone button
        |
        v
Permission request
        |
        v
Lazy model initialization
        |
        v
AudioWorklet PCM capture
        |
        v
Adaptive RMS / manual stop
        |
        v
Dedicated Worker
        |
        v
16 kHz audio resampling
        |
        v
Whisper Tiny or Base Q8
        |
        v
Compatible ONNX Runtime WASM
        |
        v
Whisper processor + tokenizer
        |
        v
Final transcript
        |
        v
Controlled React input

애플리케이션은 버전별 모델 및 런타임 파일을 자체 호스팅하고, 메인 스레드 밖에서 추론을 실행하고, 불필요한 모델 다운로드를 피하고, 사용 후 자원을 해제하고, 음성 인식을 위해 마이크 오디오를 업로드하지 않아야 합니다.

WebGPU 가속, 신경망 기반 음성 활동 감지, 연속적인 부분 전사, 추가 모델 등급 같은 고급 기능은 측정 결과를 통해 복잡성이 정당화될 때 나중에 검토할 수 있습니다.

가장 중요한 교훈은 특정 라이브러리 버전이나 모델 크기가 아닙니다. 브라우저 음성 인식 파이프라인의 모든 단계 사이 경계를 검증하는 것이 핵심입니다.

마이크 권한 요청 창이 정상으로 표시되어도 오디오가 전사된다는 뜻은 아닙니다. 모델을 다운로드해도 디코더가 초기화되었다는 뜻은 아닙니다. ONNX 세션이 성공해도 토크나이저를 사용할 수 있다는 뜻은 아닙니다. 전사에 성공해도 애플리케이션이 텍스트를 올바르게 삽입했다는 뜻은 아닙니다.

방문자가 마이크를 누르고 자연스럽게 말한 뒤 녹음을 멈추고, 인식된 내용이 편집 가능한 입력란에 표시되어야 이 기능이 완성됩니다. 그리고 그 과정에서 녹음은 음성 인식 서버로 전송되지 않아야 합니다.