현재 작업 중인 외주 개발 프로젝트에서
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에 날려주는 코드를 작성하면 된다
이번 글은 여기까지 ,,,
'기술 > 웹 개발' 카테고리의 다른 글
[react] Google Cloud text-to-speech API 이용해서 frontend에서 영어단어별 발음 재생 구현하기 (7) | 2024.10.10 |
---|---|
[react/tailwind css] 자체적으로 audio player 구현하기 (1) | 2024.10.06 |
[FastAPI/python] 파이썬 FastAPI로 정말 간단하게 API 만들기(CORS) (0) | 2024.08.31 |
[PortOne/react] 통합 결제 연동 솔루션 포트원 react 프로젝트에 이식하기 (2) | 2024.08.30 |
[node.js/express/react] 새로운 endpoint로 새로운 화면 띄우기 (1) | 2024.08.29 |