react로 음성 녹음 기능을 구현해보자

S-rim·2021년 5월 10일
18
post-thumbnail

갑자기 왜?

친구들과 프로젝트를 하던 중, 음성녹음 기능을 필요로 하는 웹을 구현하게 되었습니다. 사실 구현한 지는 작년 겨울쯤이지만...😅 거의 첫 프로젝트인데다가 react 입문 초기, 거기다 중요 기능인 음성 녹음 기능을 구현하면서 삽질을 정말 아주대박매우진짜탈모올정도로 많이 했었습니다.

예제 많지 않나? 그렇게 까지 어렵진 않을텐데... 오바 떠네...

물론 음성 녹음 예제는 많습니다. 제가 부족한 것도 있습니다. 하지만 저는 음성 녹음이 어려웠다기보다 음성 녹음 후 서버로 업로드하는 과정의 자료가 적어 그 부분에서 고생했었습니다. 혹시 녹음과 관련한 기능을 구현하는 분들이 저같이 헛고생하지 않고, 조금이나마 금방 실마리를 찾으실 수 있게 도움이 되고자 끄적여봅니다.

코드나 내용에 있는 오류는 댓글로 둥글게 말씀해주시면 감사하겠습니다 😊

Step 1. 사용자에게 허락 받기

웹에서 마이크를 사용하려면 먼저 사용자로부터 마이크 사용 권한을 획득해야 합니다. 여기서 사용자가 허용 버튼을 눌러야 이후 녹음 작업이 진행이 됩니다. 이 부분은

navigator.mediaDevices.getUserMedia({audio : true}).then(...)

이렇게 사용 권한을 획득할 수 있습니다. 여기서 허용버튼을 눌러야 then문이 실행됩니다.

Step 2. 녹음하기

사용자가 허용했을 때 녹음하는 코드를 작성해보겠습니다.

import React, { useState } from "react";

const AudioRecord = () => {
  const [stream, setStream] = useState();
  const [media, setMedia] = useState();
  const [onRec, setOnRec] = useState(true);
  const [source, setSource] = useState();
  const [analyser, setAnalyser] = useState();
  const [audioUrl, setAudioUrl] = useState();

  const onRecAudio = () => {
    // 음원정보를 담은 노드를 생성하거나 음원을 실행또는 디코딩 시키는 일을 한다
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
    // 자바스크립트를 통해 음원의 진행상태에 직접접근에 사용된다.
    const analyser = audioCtx.createScriptProcessor(0, 1, 1);
    setAnalyser(analyser);

    function makeSound(stream) {
      // 내 컴퓨터의 마이크나 다른 소스를 통해 발생한 오디오 스트림의 정보를 보여준다.
      const source = audioCtx.createMediaStreamSource(stream);
      setSource(source);
      
      // AudioBufferSourceNode 연결
      source.connect(analyser);
      analyser.connect(audioCtx.destination);
    }
    
    // 마이크 사용 권한 획득 후 녹음 시작
    navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
      const mediaRecorder = new MediaRecorder(stream);
      mediaRecorder.start();
      setStream(stream);
      setMedia(mediaRecorder);
      makeSound(stream);
	// 음성 녹음이 시작됐을 때 onRec state값을 false로 변경
      analyser.onaudioprocess = function (e) {
          setOnRec(false);
      };
    });
  };

  return (
    <>
      <button onClick={onRecAudio}>녹음</button>
      <button>결과 확인</button>
    </>
  );
};

export default AudioRecord;

유저가 녹음 버튼을 눌렀을 때 onRecAudio 함수를 실행시킵니다. 음성 녹음이 시작됐을 때 onRec state값을 false로 변경하는 이유는 녹음 버튼을 한번 더 눌렀을 때, 녹음이 중지되도록 함수를 하나 더 만들기 위해서입니다. 바로 밑에서 같이 추가해봅시다!

Step 3. 녹음 중지하기

녹음 버튼을 한번 더 눌렀을 때 녹음이 중지되도록 해보겠습니다.

  const offRecAudio = () => {
    // dataavailable 이벤트로 Blob 데이터에 대한 응답을 받을 수 있음
    media.ondataavailable = function (e) {
      setAudioUrl(e.data);
      setOnRec(true);
    };

    // 모든 트랙에서 stop()을 호출해 오디오 스트림을 정지
    stream.getAudioTracks().forEach(function (track) {
      track.stop();
    });

    // 미디어 캡처 중지
    media.stop();
    
    // 메서드가 호출 된 노드 연결 해제
    analyser.disconnect();
    source.disconnect();
  };
return (
    <>
    // onRec 값을 통해 녹음 중인지 아닌지를 구분하여 함수 실행
      <button onClick={onRec ? onRecAudio : offRecAudio}>녹음</button>
      <button>결과 확인</button>
    </>
  );

Step 4. 조건에 맞춰서 녹음하기

음성 녹음 기능을 구현하면서 조건을 넣을 수 있습니다. 저는 3분까지만 녹음을 할 수 있도록 하는 조건을 추가해보겠습니다. e.playbackTime 은 녹음이 되고있는 시간을 초 단위로 나타냅니다! 3분이 지났을때 음성 녹음이 중지되는 조건을 추가하고 싶으니 180으로 조건문을 작성하면 되겠죠?

analyser.onaudioprocess = function (e) {
        // 3분(180초) 지나면 자동으로 음성 저장 및 녹음 중지
        if (e.playbackTime > 180) {
          stream.getAudioTracks().forEach(function (track) {
            track.stop();
          });
          mediaRecorder.stop();
          // 메서드가 호출 된 노드 연결 해제
          analyser.disconnect();
          audioCtx.createMediaStreamSource(stream).disconnect();

          mediaRecorder.ondataavailable = function (e) {
            setAudioUrl(e.data);
            setOnRec(true);
          };
        } 
        
        else {
          setOnRec(false);
        }
      };

Step 5. 녹음 종료 후 원하는 방식으로 처리하기

사용자가 녹음을 종료하고 결과 확인 버튼을 눌렀을 때 audio url 또는 파일로 변환시켜 console에 출력해보겠습니다. 그럼 먼저 url로 생성하는 방식부터 확인해봅시다!

const onSubmitAudioFile = useCallback(() => {
    if (audioUrl) {
      console.log(URL.createObjectURL(audioUrl)); // 출력된 링크에서 녹음된 오디오 확인 가능
    }
  }, [audioUrl]);

그럼 파일 형식으로 생성하는 과정을 알아보겠습니다. 보통 서버에 파일을 업로드할 때는 form-data 방식을 사용합니다. 그렇기 때문에 음성 파일로 업로드해줘야합니다. 쉽게 생각하면 금방 해결됩니다. File생성자를 이용하면 됩니다.

const onSubmitAudioFile = useCallback(() => {
    if (audioUrl) {
      console.log(URL.createObjectURL(audioUrl)); // 출력된 링크에서 녹음된 오디오 확인 가능
    }
    // File 생성자를 사용해 파일로 변환
    const sound = new File([audioUrl], "soundBlob", { lastModified: new Date().getTime(), type: "audio" });
    console.log(sound); // File 정보 출력
  }, [audioUrl]);

해당 함수를 결과 확인 버튼을 클릭했을 때 실행시켜줍니다.

<button onClick={onSubmitAudioFile}>결과 확인</button>

이제 음성 녹음 후 결과 확인 버튼을 클릭했을 때 console창에서 오디오의 url과 파일 정보를 확인할 수 있습니다.


짜잔! 저 blob url로 들어가면 오디오를 재생해볼 수 있습니다😆

저 url을 audio태그의 src로 적용하면 웹페이지에서 재생시킬 수도 있겠죠? 또는 파일정보를 form-data에 삽입하여 서버에 보낼 수 있습니다! 필요한 방식에 맞춰 자유롭게 활용하시길 바랍니다🤗

전체 코드입니다!

import React, { useState, useCallback } from "react";

const AudioRecord = () => {
  const [stream, setStream] = useState();
  const [media, setMedia] = useState();
  const [onRec, setOnRec] = useState(true);
  const [source, setSource] = useState();
  const [analyser, setAnalyser] = useState();
  const [audioUrl, setAudioUrl] = useState();

  const onRecAudio = () => {
    // 음원정보를 담은 노드를 생성하거나 음원을 실행또는 디코딩 시키는 일을 한다
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    // 자바스크립트를 통해 음원의 진행상태에 직접접근에 사용된다.
    const analyser = audioCtx.createScriptProcessor(0, 1, 1);
    setAnalyser(analyser);

    function makeSound(stream) {
      // 내 컴퓨터의 마이크나 다른 소스를 통해 발생한 오디오 스트림의 정보를 보여준다.
      const source = audioCtx.createMediaStreamSource(stream);
      setSource(source);
      source.connect(analyser);
      analyser.connect(audioCtx.destination);
    }
    // 마이크 사용 권한 획득
    navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
      const mediaRecorder = new MediaRecorder(stream);
      mediaRecorder.start();
      setStream(stream);
      setMedia(mediaRecorder);
      makeSound(stream);

      analyser.onaudioprocess = function (e) {
        // 3분(180초) 지나면 자동으로 음성 저장 및 녹음 중지
        if (e.playbackTime > 180) {
          stream.getAudioTracks().forEach(function (track) {
            track.stop();
          });
          mediaRecorder.stop();
          // 메서드가 호출 된 노드 연결 해제
          analyser.disconnect();
          audioCtx.createMediaStreamSource(stream).disconnect();

          mediaRecorder.ondataavailable = function (e) {
            setAudioUrl(e.data);
            setOnRec(true);
          };
        } else {
          setOnRec(false);
        }
      };
    });
  };

  // 사용자가 음성 녹음을 중지했을 때
  const offRecAudio = () => {
    // dataavailable 이벤트로 Blob 데이터에 대한 응답을 받을 수 있음
    media.ondataavailable = function (e) {
      setAudioUrl(e.data);
      setOnRec(true);
    };

    // 모든 트랙에서 stop()을 호출해 오디오 스트림을 정지
    stream.getAudioTracks().forEach(function (track) {
      track.stop();
    });

    // 미디어 캡처 중지
    media.stop();
    // 메서드가 호출 된 노드 연결 해제
    analyser.disconnect();
    source.disconnect();
  };

  const onSubmitAudioFile = useCallback(() => {
    if (audioUrl) {
      console.log(URL.createObjectURL(audioUrl)); // 출력된 링크에서 녹음된 오디오 확인 가능
    }
    // File 생성자를 사용해 파일로 변환
    const sound = new File([audioUrl], "soundBlob", { lastModified: new Date().getTime(), type: "audio" });
    console.log(sound); // File 정보 출력
  }, [audioUrl]);

  return (
    <>
      <button onClick={onRec ? onRecAudio : offRecAudio}>녹음</button>
      <button onClick={onSubmitAudioFile}>결과 확인</button>
    </>
  );
};

export default AudioRecord;
profile
🤗

1개의 댓글

comment-user-thumbnail
2023년 7월 4일

첫 사진 너무 웃겨서 댓글 달아요ㅋㅋㅋㅋㅋㅋㅋ
많은 도움 됐습니다 :)

답글 달기