이번 프로젝트에서는 음성을 텍스트로 변환하고(STT) 달리에게 텍스트를 기반으로 그림을 그려달라고 요청하는(Open AI API) 서비스를 구현하고 있다
그 과정에서 form data를 처음 다뤄보게 되었고, 특히 여러 파일을 multipart로 한 번에 보내면서 정말 며칠을 헤맸던 것 같다
생각보다 자료가 많이 없어서 스택오버플로우와 깃허브 이슈, 미디엄, 코드샌드박스, 하다하다 무슨 중국어로 된 qna 사이트까지 다 뒤져가면서 구글링했다 하하
처음에는 한국어로 된 블로그를 보고 녹음 기능을 생각보다 금방 구현해서 오! 간단한데? 라고 생각.. 했으나 (사망 플래그)
mp3로 파일을 변환하는 과정이나 서버로 파일을 보내는 과정 등을 확인할 수 없어서 직접 이것저것 찾아보며 구현했고
기존 코드에 대한 이해도가 높지 않다보니 변환한 mp3 파일이 비어있는 등 여기저기서 문제가 발생했다
어찌저찌 하나씩 문제를 해결해서 길고 복잡한.. 코드가 만들어졌지만, 내 콘솔에는 분명 mp3 파일이 찍히고 다운로드도 되는데 백에서는 유효한 파일 형식이 아니라는 응답이 오는 등... 머리 깨질 것 같은 에러가 반복됐다
주변 현직자분께도 질문 드리고 qna도 해보고 스스로도 찾아보면서 문제점을 발견하려고 했으나, 이미 코드가 너무나 길고 방대하고.. 하고자 하는 기능이 너무나 많아서 문제점이 잘 보이지 않았다
이때 현직자분께서 이런 조언을 해주셨다
너무 어려울 때는, 오히려 처음부터 다시 시작해봐!
자....그래서 처음부터 다시 시작했다
우선은 기능 구현이 주 목적이고, 해커톤 마감까지 남은 기간이 얼마 없기 때문에 완성을 최우선으로 구현했다. 그래서 구조가 가장 효율적인 방식은 아닐수도 있다는 점 감안 부탁..!
아래는 녹음 기능 + mp3 파일 서버 전송 + 응답을 받는 전체 코드이다.
사실 질문이 여러 개라 다른 페이지가 더 있는데, 주요한 기능은 여기서 다 처리해서 이 코드를 첨부했다.
import React from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import useStore from "../../../state/store";
import MicRecorder from "mic-recorder-to-mp3";
import * as S from "../Word.style";
import * as W from "./VoiceRecord.style";
const RcThird = (e) => {
const navigate = useNavigate();
const {
setAudioUrl,
keyword,
audioData,
isRecording,
setIsRecording,
recorder,
setRecorder,
setPictureDiary,
setTextDiary,
} = useStore((state) => state);
const accessToken = localStorage.getItem("accessToken");
const recordApiUrl = "api 주소를 이곳에 입력해주시면 됩니다";
const startRecording = async () => {
const mp3Recorder = new MicRecorder({ bitRate: 128 });
try {
await mp3Recorder.start();
setRecorder(mp3Recorder);
setIsRecording(true);
} catch (error) {
console.error(error);
}
};
const stopRecording = async () => {
try {
const [buffer, blob] = await recorder.stop().getMp3();
const file = new File(buffer, "3rd_q.mp3", {
type: blob.type,
lastModified: Date.now(),
});
if (file.size === 0) {
console.log("파일이 비어있거나 유효하지 않습니다");
return;
}
const formData = new FormData();
formData.append("keyword", keyword);
formData.append("firstAnswer", audioData.get("firstAnswer"));
formData.append("secondAnswer", audioData.get("secondAnswer"));
formData.append("thirdAnswer", file);
console.log("formData:", ...formData);
setAudioUrl(URL.createObjectURL(file));
setIsRecording(false);
if (accessToken) {
axios
.post(recordApiUrl, formData, {
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${accessToken}`,
},
})
.then((response) => {
console.log("음성 파일 업로드 성공 ✨", response.data);
setTextDiary(response.data.textDiary);
setPictureDiary(response.data.pictureDiary);
})
.catch((error) => {
console.error("음성 파일 업로드 실패:", error);
});
} else {
console.log("액세스 토큰이 존재하지 않습니다");
}
} catch (error) {
console.error(error);
}
};
const handleButtonClick = () => {
if (isRecording) {
stopRecording();
navigate("/textResult");
} else {
startRecording();
}
};
return (
<W.VoiceRecordContainer>
<S.TodayWordContainer>
<S.TodayWord>
{(function () {
if (keyword === "가족")
return <S.TodayWordContent src="/images/text_family.png" />;
if (keyword === "친구")
return <S.TodayWordContent src="/images/text_friend.png" />;
if (keyword === "여행")
return <S.TodayWordContent src="/icons/text_travel.png" />;
})()}
</S.TodayWord>
</S.TodayWordContainer>
<W.VoiceRecordTitle>
누구와 어떤 하루를 <br /> 보내셨나요?
</W.VoiceRecordTitle>
{isRecording ? (
<>
<W.ListeningText>귀기울여 듣고 있어요</W.ListeningText>
<W.StartRecordButton onClick={handleButtonClick} value={1}>
말 끝내기
</W.StartRecordButton>
</>
) : (
<W.StartRecordButton onClick={handleButtonClick} value={0}>
말 시작하기
</W.StartRecordButton>
)}
<W.CancelButton onClick={() => navigate("/word")}>
취소하기
</W.CancelButton>
</W.VoiceRecordContainer>
);
};
export default RcThird;
우선 녹음한 음성을 mp3 파일로 변환하기 위해 아래 라이브러리를 설치해서 import 해준다
import MicRecorder from "mic-recorder-to-mp3";
상태관리는 zustand를 사용하고 있기 때문에, store 폴더에 저장해둔 state를 불러온다
(여담이지만 zustand 진짜 짱이다....)
const {
setAudioUrl,
keyword,
audioData,
isRecording,
setIsRecording,
recorder,
setRecorder,
setPictureDiary,
setTextDiary,
} = useStore((state) => state);
간단하게 녹음을 구현해준다. bitRate는 128로 해뒀는데 때에 따라 오류가 나면 낮춰야할 수도 있다.
가끔 여기서 오류가 나는 사람도 있다길래..!
const startRecording = async () => {
const mp3Recorder = new MicRecorder({ bitRate: 128 });
try {
await mp3Recorder.start();
setRecorder(mp3Recorder);
setIsRecording(true);
} catch (error) {
console.error(error);
}
};
자 이제 stopRecording 함수를 작성해준다 (여기서 대부분의 시간을 썼다)
글로 쓰기에는 애매할 것 같아서 주석으로 설명을 작성해두었다
const stopRecording = async () => {
try {
const [buffer, blob] = await recorder.stop().getMp3();
const file = new File(buffer, "3rd_q.mp3", {
type: blob.type,
lastModified: Date.now(),
});
// 빈 파일이거나 유효하지 않을 경우 콘솔에 표시
if (file.size === 0) {
console.log("파일이 비어있거나 유효하지 않습니다");
return;
}
// 새로운 formData 생성
const formData = new FormData();
// formData에 필요한 내용을 추가해준다
formData.append("keyword", keyword);
formData.append("firstAnswer", audioData.get("firstAnswer"));
formData.append("secondAnswer", audioData.get("secondAnswer"));
formData.append("thirdAnswer", file);
// formData는 그냥 콘솔에 찍으면 빈 객체로 나오므로 펼쳐서 내용을 확인한다
console.log("formData:", ...formData);
setAudioUrl(URL.createObjectURL(file));
setIsRecording(false);
// 액세스 토큰이 있을 경우 API 통신 진행
if (accessToken) {
axios
.post(recordApiUrl, formData, {
headers: {
"Content-Type": "multipart/form-data", // 여러 파일을 form data로 전송
Authorization: `Bearer ${accessToken}`, // 액세스 토큰을 헤더에 담아서 전송
},
})
.then((response) => {
console.log("음성 파일 업로드 성공 ✨", response.data); // 응답 데이터 출력
setTextDiary(response.data.textDiary); // STT 결과 데이터
setPictureDiary(response.data.pictureDiary); // AI 그림 데이터
})
.catch((error) => {
console.error("음성 파일 업로드 실패:", error);
});
} else {
console.log("액세스 토큰이 존재하지 않습니다");
}
} catch (error) {
console.error(error);
}
};
그리고 마지막으로 버튼을 누르면 진행할 액션에 대해서 별도의 함수를 만들어 관리해준다
나는 녹음을 마치면 서버에 파일을 보내고, 응답을 받은 후에 STT 결과를 보여주는 페이지로 넘어가도록 설정했다
const handleButtonClick = () => {
if (isRecording) {
stopRecording();
navigate("/textResult");
} else {
startRecording();
}
};
'Web > React' 카테고리의 다른 글
[React] 라우터 구현하기 + reset css 설치 (0) | 2022.11.04 |
---|---|
Vercel 로 React 프로젝트 배포하기 (0) | 2022.10.27 |
[React] 리액트 설치와 세팅법 (2) | 2022.09.20 |