Web API로 음성 녹음 기능 만들기 (MediaRecorder 활용)

June 15, 2025

웹에서 음성을 녹음하고 파일로 저장하는 기능은 MediaRecorder API를 활용하면 비교적 간단하게 구현할 수 있습니다. React 기반 프로젝트에서 마이크 입력을 받아 녹음하고, 다운로드 가능한 오디오 파일로 만드는 흐름을 훅 형태로 정리해보겠습니다.


구현예시

1. VoiceRecorder

//voice-recorder.tsx
import { useState } from "react";
import { useVoiceRecorder } from "@/hooks/use-voice-recorder";
import { downloadBlob } from "@/utils/download-blob";

export default function VoiceRecorder() {
  const [audioBlob, setAudioBlob] = useState<Blob>();
  const { isRecording, startRecording, stopRecording } = useVoiceRecorder({
    onComplete: (blob) => {
      console.log("녹음 완료!", blob);
      setAudioBlob(blob);
    },
  });

  const handleDownload = () => {
    if (audioBlob) {
      downloadBlob(audioBlob, `voice-${Date.now()}.webm`);
    }
  };

  const handleToggleRecording = () => {
    if (isRecording) {
      stopRecording();
    } else {
      startRecording();
    }
  };

  return (
    <div className="flex flex-col items-center justify-center h-screen gap-4 px-4 text-center">
      <h1 className="text-2xl font-bold">Voice Recorder</h1>
      <button
        className={`px-4 py-2 text-white rounded cursor-pointer w-48 ${
          isRecording ? "bg-red-500" : "bg-blue-500"
        }`}
        onClick={handleToggleRecording}
      >
        {isRecording ? "Stop" : "Start"} Recording
      </button>
      <button
        className="px-3 py-1 bg-green-500 text-white rounded disabled:opacity-50"
        onClick={handleDownload}
        disabled={!audioBlob}
      >
        📥 다운로드
      </button>
    </div>
  );
}

2. useVoiceRecorder

//use-voice-recorder.ts
import { useRef, useState } from "react";

export function useVoiceRecorder({
  onComplete,
}: {
  onComplete: (blob: Blob) => void;
}) {
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const [isRecording, setIsRecording] = useState(false);
  const chunksRef = useRef<Blob[]>([]);

  const startRecording = async () => {
    if (!navigator.mediaDevices?.getUserMedia) {
      console.error("getUserMedia not supported");
      return;
    }

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const recorder = new MediaRecorder(stream);

      chunksRef.current = [];

      recorder.ondataavailable = (e) => {
        chunksRef.current.push(e.data);
      };

      recorder.onstop = () => {
        const blob = new Blob(chunksRef.current, { type: "audio/webm" });
        onComplete(blob);
        stream.getTracks().forEach((track) => track.stop());
      };

      mediaRecorderRef.current = recorder;
      recorder.start();
      setIsRecording(true);
    } catch (err) {
      console.error("Microphone error", err);
    }
  };

  const stopRecording = () => {
    if (mediaRecorderRef.current?.state === "recording") {
      mediaRecorderRef.current.stop();
      setIsRecording(false);
    }
  };

  return {
    isRecording,
    startRecording,
    stopRecording,
  };
}

3. downloadBlob

//download-blob.ts
export function downloadBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");

  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();

  setTimeout(() => {
    URL.revokeObjectURL(url);
    document.body.removeChild(a);
  }, 100);
}

요약 설명

1. Media Stream 요청

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  • 브라우저가 마이크 접근 권한을 요청하며, 이 스트림은 녹음에 사용됩니다.
  • video까지 녹음하려면 { video: true }도 가능.

2. MediaRecorder 사용

  • MediaRecorderMediaStream을 입력받아 오디오/비디오 녹음을 관리합니다.
  • start(), stop() 등의 메서드를 통해 녹음을 제어

3. ondataavailable 이벤트

  • 녹음 중 일정 시간마다 또는 stop() 시점에 호출되는 이벤트
  • recorder.start(1000)으로 설정 시, 1초마다 데이터를 chunk 단위로 받을 수 있습니다.
    • 실시간 스트리밍 처리, 대용량 데이터 나눠 저장, 음성 인식 등 실시간 처리 등에 사용

4. 스트림 종료 처리

stream.getTracks().forEach((track) => track.stop());
  • mediaRecorder.stop()만으로는 브라우저 상단의 마이크 사용 아이콘이 사라지지 않거나, 스트림이 계속 유지될 수 있습니다.
  • 따라서 명시적으로 마이크 트랙을 종료해주어 리소스를 해제합니다.

작동 예제

Reference