오늘은 원래 만들고 싶었던 음악 퀴즈(?) 기능을 만들었다. Spotify에 음악 데이터(예: 아이유)를 요청하면 데이터에 preview_url이 포함되어 있는데 음원을 30초 미리 들을 수 있다.
기존에 만들려고 했던 것은 음악을 자동으로 일정 시간 5초, 10초, 20초 이런 식으로 재생을 시키고 사용자가 자기가 생각하는 노래의 제목을 제출하면 정답인지 아닌지 판별하려고 했다. 하지만 노래의 제목이 내가 원한 것처럼 다 한국어로 되어 있는 것도 아니고 차라리 30초 음원을 전부 재생을 시킬 수 있지만 사용자가 어느 정도 들었을 때 노래를 멈추고 확인 버튼을 누르면 정답을 알 수 있도록 만들기로 결정했다.
기존의 라이브러리 플레이어가 아닌 audio 태그를 사용해서 작동을 시켜야 한다.
기존의 스타일링과 preview_url 데이터는 준비가 돼있다는 전제하에 작성한다.
우선 audio 태그 자체를 스타일링해서 사용하는 것이 아니기 때문에 아닌 useRef를 이용해 audioRef 객체를 만들었다.
추가로 현재 실행 중인 상태, 노래의 인덱스, Progress를 관리하기 위해 상태 값들을 만들어 주었다.
const [isPlaying, setIsPlaying] = useState(false);
const [activeSongIndex, setActiveSongIndex] = useState(0);
const [currentProgress, setcurrentProgress] = useState(0);
const audioRef = useRef<HTMLAudioElement>(null);
<audio
ref={audioRef}
src={songs[activeSongIndex].previewUrl}
onTimeUpdate={onPlaying}
/>
재생 버튼을 누르면 현재 실행중인지 아닌지를 업데이트 하는 함수를 구현했다.
const handlePlayPause = () => {
setIsPlaying(!isPlaying);
};
실제 audio 음악 재생은 useEffect를 사용하여 구현했다. 버튼을 누르면 isPlaying이 true이기 때문에 실행 아니면 중단한다. if 문은 현재 타입 스크립트를 사용하고 있는데 if 문 처리해 주지 않으면 null 가능성이 있다고 에러가 발생한다.
useEffect(() => {
if (audioRef.current !== null) {
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}
}, [isPlaying]);
노래 진행 상태를 나타내기 위한 onPlaying 함수를 구현하고 audio 태그에 onTimeUpdate 속성에 전달해 주었다. audioRef.current.duration으로 현재 듣고 있는 노래의 시간을 구하고 audioRef.current.currentTime 진행 중인 시간을 나타낸다고 보면 된다. 전체 시간으로 진행 중인 시간을 나누고 백분율로 나타내기 위해 100을 곱해준다. 구한 값으로 progress를 상태 값을 지속적으로 업데이트해준다. progressBar div에 inline 스타일로 width 값을 할당하는 것을 확인할 수 있다.
progressBar 코드
<div
className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700"
onClick={handleCheckDuration}
ref={durationRef}
>
<div className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${currentProgress}%` }}
>
</div>
</div>
onPlaying 함수
const onPlaying = () => {
if (audioRef.current !== null) {
const duration = audioRef.current.duration;
// console.log(duration);
const currentTime = audioRef.current.currentTime;
// console.log(currentTime);
const progress = (currentTime / duration) * 100;
setcurrentProgress(progress);
}
};
<audio
ref={audioRef}
src={songs[activeSongIndex].previewUrl}
onTimeUpdate={onPlaying}
/>
progressBar를 클릭했을 때 노래를 재생시키기 위해 useRef 객체를 하나 추가했다.
const durationRef = useRef<HTMLDivElement>(null);
const handleCheckDuration = (e: React.MouseEvent<HTMLDivElement>) => {
if (durationRef.current !== null && audioRef.current !== null) {
let width = durationRef.current.clientWidth;
const offset = e.nativeEvent.offsetX;
const progress = offset / width;
audioRef.current.currentTime =
progress * audioRef.current.duration;
}
};
두 ref 객체가 다 필요하기 때문에 if 문에서 처리를 해주었다. 그다음 durationRef.current.clientWidth는 progressbar의 전체 길이이다. 아래는 예시 사진이다. 여기서 이제 e.nativeEvent.offsetX은 내가 progressBar를 클릭했을 때 얼마나 거리가 떨어져 있는지 나온다고 보면 된다. 결국 전체 길이로 진행된 부분을 나눠준 퍼센티지로 원래의 노래 시간을 곱하면 실제 노래 진행 시간이 맞춰진다.
음악을 재생/일시 중지도 가능해야 하지만 이전 음악을 듣거나 다음 음악으로 넘어갈 수 있어야 한다. 이 부분은 비교적 쉽다. 아래 코드는 이전 노래를 재생하게 하는 코드인데 인덱스 값을 보고 인덱스가 초기 값(0)이라면 젤 끝부분으로 돌아가게 만들고 아니면 현재 인덱스 - 1 해준다. Next 기능은 반대로 만들면 된다. 노래 현재 시간 0, progress 진행도, setShowAnswer은 다시 언급하겠다.
const handleSkipBack = () => {
if (activeSongIndex === 0) {
setActiveSongIndex(songs.length - 1);
} else {
setActiveSongIndex((prevIndex) => prevIndex - 1);
}
if (audioRef.current !== null) {
audioRef.current.currentTime = 0;
}
setcurrentProgress(0);
setShowAnswer(initalAnswer);
};
디자인은 아직 별로지만 저기서 정답 확인을 누르면 나오는 노래의 이미지와 노래 제목이 나타나게 만들었다. 처음 보이는 이미지는 public 폴더에 있는 물음표 이미지 경로를 설정하고 노래 제목은 "???"값으로 설정했다. 위에서 handleSkipBack 함수의 setShowAnswer(initalAnswer); 부분은 이전 노래로 변경했을 때 다시 이미지와 노래 제목을 숨기는 역할을 한다.
초기 설정 값
const initalAnswer = {
imageSrc: "/question-mark.png",
songName: "???",
};
const [showAnswer, setShowAnswer] = useState(initalAnswer);
상태 변경 함수
const handleCheckAnswer = () => {
setShowAnswer({
imageSrc: songs[activeSongIndex].imageUrl,
songName: songs[activeSongIndex].songName,
});
};
하나하나 기능을 추가할 때마다 배우는 부분도 많으면서 이런 식으로 구현해도 되는지 스스로 질문을 던지는 것 같다. 어떻게 해야 된다는 정해진 정답은 없겠지만 효율적인 방법은 존재한다고 생각하기 때문에 이 부분에 대해서 고민을 많이 해야 할 것 같다고 생각이 든다.