기술/웹 개발

[react] client단에서 마이크를 이용해 녹음한 파일 API로 전송하기(multipart 전송)

하기싫지만어떡해해야지 2024. 9. 5. 16:12

현재 작업 중인 외주 개발 프로젝트에서

user가 영어 문장을 컴퓨터 마이크를 통해 녹음하면

해당 녹음 파일을 발음평가 API로 전송해

user의 발음 점수를 받아오는 기능을

구현해야했다.

 

따라서 유저가 녹음한 파일을 API를 통해

백엔드 서버에 보낸 뒤,

백엔드에서 발음평가 API로 다시 전송해서

점수를 받아오고 이를 다시 프론트에서 받아와서

화면에 뿌려줘야했다

 

정말 이 기능 구현하면서

삽질을 너무너무너무너무너무 많이해서

시간낭비를 제대로 했지만 ^^;

 

이 부분에서 삽질을 한 건 아니라서

이 것도 까먹기 전에 얼른 기록으로 남겨두려한다


1. recording 상태 확인 할 useState정의

우선 record 기능을 컨트롤할

버튼을 한 개 구현해줬다

 

recording이 아닐 때는

Button의 텍스트가

"Start Recording"이었다가

recording 중이면

"Stop Recording"으로 변하게 해주고싶었다

 

그래서 상단에

const [recording, setRecording] = useState(false);

 

useState로 recording상태를 담을 변수와

recording이 끝났는지 여부를 담을 변수를 정의해주었다

 

화면에 처음 진입했을 땐

당연히 녹음 중이 아니므로

기본값은 false로 정의해주었다

 

 

2. Recording Button 컴포넌트 생성

그런 다음 recording Button

컴포넌트를 작성해주었다

import React from 'react';

const RecordButton = ({ recording, onStart, onStop }) => {
  return (
    <button
      onClick={recording ? onStop : onStart}
      className={`px-6 py-3 rounded-lg border border-solid shadow-md text-white
        ${recording ? 'bg-red-600 hover:bg-red-700 border-red-700' : 'bg-green-600 hover:bg-green-700 border-green-700'}
      `}
    >
      {recording ? 'Stop Recording' : 'Start Recording'}
    </button>
  );
};

export default RecordButton;

 

우선 위에서 정의한 recording boolean변수와

녹음이 시작될 때 실행하게 할 콜백함수 onStart,

녹음이 끝났을 때 실행하게 할 콜백함수 onStop을

파라미터로 받아오게 해주었다

 

recording변수가 true이면

'onStart' 함수가 실행되고

'Stop Recording'이 텍스트로 뜬다

 

recording이 false이면

'onStop' 함수가 실행되고

'Start Recording'이 텍스트로 뜬다

 

또한 recording true, false 여부에 따라

버튼 색도 바뀌게 해주었다

 

이렇게 button이 완성되었다

 

3. 녹음 onStart, onStop handler

위에서 컴포넌트로 생성한 녹음 버튼에

user가 녹음을 하려고 버튼을 누르면

녹음이 시작되는 handler와

녹음을 끝내고 싶을때 클릭하면

녹음이 끝나는 handler를

파라미터로 받도록 정의해주었다

 

그럼 이제 이 두가지 핸들러를

구현해줘야한다

 

4. onStart

우선 녹음이 시작될 때 부터 정의해주자

당연히 react에서 마이크를 이용해서

녹음을 받아올 객체부터 정의해줘야한다

 

우선 client단에서 마이크를 이용해서

녹음을 받아줄 객체는 다음과 같이 정의해줬다

const mediaRecorderRef = useRef(null);

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

 

navigator에서 getUserMedia를 호출하고

audio를 true로 하면

user가 마이크를 이용해 녹음하는 것을

stream형태로 받아올 수 있다

 

이를 사전에 useRef로 정의해 둔

mediaRecorderRef에

current로 새로운 MediaRecorder를 생성한 뒤

파라미터로 이 stream을 넣어준다

 

그럼 만약 user가 녹음을 시작하면

mediaRecorderRef.current.ondataavailable = (event) => {
  if (event.data.size > 0) {
  // 녹음 data
    console.log(event.data);
  }
};

 

위와 같은 방식으로

event.data를 통해

user가 녹음한 데이터를 받아올 수 있다

 

나는 이 event.data를 받을 useRef변수를

위에 하나 더 생성해주었다

const recordedChunksRef = useRef([]);

mediaRecorderRef.current.ondataavailable = (event) => {
  if (event.data.size > 0) {
    recordedChunksRef.current.push(event.data);
  }
};

 

그럼 녹음이 시작되면

user가 녹음하고 있는 데이터가

recordedChunksRef의 array에

담기게 된다

 

또한, 녹음 시작버튼이 눌리게 되면

녹음이 시작되는 것이므로

앞에서 정의해줬던 recording 변수를

false에서 true로 설정해줘야한다

setRecording(true);

 

따라서 onStart 함수 가장 위에

setRecording(true)로

recording을 true로 설정해줬다

 

onStart 핸들러 전체 코드는 아래와 같다

  const handleStartRecording = async () => {
    setRecording(true);

    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    mediaRecorderRef.current = new MediaRecorder(stream);
    recordedChunksRef.current = [];

    mediaRecorderRef.current.ondataavailable = (event) => {
      if (event.data.size > 0) {
        recordedChunksRef.current.push(event.data);
      }
    };

    mediaRecorderRef.current.start();
  };

 

 

5. onStop

녹음이 시작되면 recording 변수를 변경해주고

user의 마이크로 들어오는 음성을

recoredChunksRef라는 array변수에 담아

담아주는 것까지 마쳤다

 

그럼 이제 user가 녹음을 마쳤을 때

어떻게 작동하면 좋을지 handler 함수를

작성해보자

 

우선 함수를 작성하기 전에

녹음을 마쳤을 때

어떤 기능을 구현할건지를 정의해야한다

 

나는 녹음을 마치면

녹음된 파일을 backend로 전송하도록

multipart/form-data 요청을 날려야했다

 

그럼 user가 녹음을 마쳤을 때

녹음 파일을 backend에 보내는 요청을 날리려면

어떤 과정이 필요할지 생각해보자

 

1. user가 녹음을 마치는 순간까지

저장된 녹음 데이터를 확인

2. 해당 녹음 데이터를 API 전송 가능한

형태로 바꾸기

3. 위 과정이 완료되었을 때,

API 요청 날리기

 

크게 이 3가지 정도로 정리될 것 같다

 

이 3가지를 어디에다 어떤 방식으로 구현할지

큰 그림을 코드로 표현해봤다

// mediaRecorderRef.current(녹음 stream)에 데이터가 존재하면
if (mediaRecorderRef.current) {
	// 녹음을 중지하고
      mediaRecorderRef.current.stop();
      // 녹음이 중지가 되었을 때 비동기 콜백함수 정의
      mediaRecorderRef.current.onstop = async () => {
      // 이 부분에서 지금까지 녹음된 데이터 받아온 다음 API 전송가능한 형태로 변경해서
      // POST request parameter 만들어주기
        try {
          // API request날리는 코드 작성
        } catch (error) {
          console.error("There was an error transcribing the audio!", error);
        }
      };
    }

 

 

코드의 큰 구조는 다음과 같다

callback 지옥이라 느낄 수 있는데

프론트엔드 작업은

원래 콜백지옥,,,

 

 

그럼 이제 녹음 데이터를 어떻게 변형해서

API 파라미터로 넘겨줄까?

 

1) 녹음된 데이터 Blob객체로 변경

Blob이라는 객체에 담아야한다

Blob은 javascript에서 파일이나 이진 데이터를

나타내는 객체로, 주로 네트워크로 파일을 전송하거나

이미지, 텍스트, 오디오 등 다양한 유형의 데이터를

처리할 때 사용된다

 

나는 녹음된 파일을 wav형태로 보내줘야했으므로

이를 Blob으로 저장하는 코드는 다음과 같다

// recordedChunksRef.current는 지금까지 녹음된 데이터를 담은 array
const blob = new Blob(recordedChunksRef.current, { type: 'audio/wav' });

 

 

2) multipart/form-data 전송

이렇게해서 녹음된 데이터를 이진데이터는 Blob 객체로 생성해주었으면

mulitpart 전송을 위해 form-data라는 객체에

이 blob을 담아주어야한다

 

mulitpart/form-data는 주로 웹에서

파일 전송이나 복잡한 데이터 전송을 위해 사용되는

HTTP 프로토콜이다

 

파라미터로 보내는 데이터가

다양한 형태를 가질 때 주로 사용한다

 

보내는 법은 매우 간단한데

javascript에서는 formData객체를 생성해주고

생성한 formData객체에 파라미터로 보낼 변수들을

append해준다

 

그런다음 request 날릴 때

header의 Content-Type에

multipart/form-data

를 작성해서 보내주면 된다

 

코드는 아래와 같다

const blob = new Blob(recordedChunksRef.current, { type: 'audio/wav' });
const formData = new FormData();
formData.append('file', blob, 'recording.wav');

axios.post(`${API_BASE_URL}/endpoint`, formData, {
  headers: { 'Content-Type': 'multipart/form-data' },
}).then((response)=> {
  // 요청 성공했을 때, response로 실행될 callback 함수 작성해주는 부분
})

 

그래서 구현한 전체 함수는 아래와 같다

const handleStopRecording = () => {
    setRecording(false);
    
    if (mediaRecorderRef.current) {
      mediaRecorderRef.current.stop();
      mediaRecorderRef.current.onstop = async () => {
        const blob = new Blob(recordedChunksRef.current, { type: 'audio/wav' });
        const formData = new FormData();
        formData.append('file', blob, 'recording.wav');
        try {
            axios.post(`${API_BASE_URL}/api/endpoint`, formData, {
              headers: { 'Content-Type': 'multipart/form-data' },
            }).then((response)=> {
            // response받아서 동작하는 부분
              const jsonResult = JSON.parse(response.data);
              console.log(jsonResult);
              setAzureAiScore(jsonResult);
              recordedChunksRef.current = [];
            })
        } catch (error) {
          console.error("There was an error transcribing the audio!", error);
        }
      };
    }
  };

 

이렇게 정의한 콜백함수들을

맨처음에 만들어준 Button Component들에

파라미터로 담아준다

<RecordButton
    recording={recording}
    onStart={handleStartRecording}
    onStop={handleStopRecording}
    buttonText={
        recording ? "Stop Recording" : "Start Recording"
    }
/>

 

이렇게 구현하면

user가 stop recording 버튼을

누름과 동시에

그 순간까지 녹음이 멈추고

녹음이 멈출 때까지 저장된 녹음 데이터들이

wav형태로 저장되어

form-data에 담겨서

백엔드로 보내지게 된다

 

 

 

 

 

이렇게 frontend에서 wav형태의 파일을 날려주면,

해당 파일을 백엔드에서 좀 더 가공해서

발음평가 API에 날려주는 코드를 작성하면 된다

 

 

이번 글은 여기까지 ,,,