Youtube API로 Next.js 블로그에 BGM 만들기
react-youtube 라이브러리로 저렴한 Zukebox 만들기
서론
Frutiger Aero 컨셉에 맞게 옛 블로그 느낌을 내고 싶어서 bgm이 나오길 원했습니다.
다양한 방법 중, 프로젝트 내에 음악파일을 넣지 않고도 YouTube API를 사용해 임베디드 하는 방식을 취했습니다.
YouTube IFrame Player API를 React에서 손쉽게 사용할 수 있는 react-youtube 라이브러리를 사용해서 저렴하게 ZukeBox를 만들었습니다.
블로그에 나올 노래들은 YouTube에서 재생목록을 생성하여 추가하면 됩니다.
react-youtube
react-youtube
는 YouTube IFrame Player API를 간편하게 사용할 수 있도록 제공해주는 라이브러리입니다.
YouTube IFrame Player API는 웹 사이트를 통해 YouTube 동영상 플레어이를 퍼가고, JavaScript를 사용하여 플레이어를 제어할 수 있습니다. <iframe>태그를 사용하여 콘텐츠를 페이지에 게시합니다.
JavaScript함수를 사용하여 영상 재생, 일지중지와 플레이어 볼륨을 조절할 수 있습니다.
본래 페이지 내 YouTube 영상을 <iframe> 태그로 임베디드함이 목적이지만, 영상이 아닌 음성만 나오도록 하였습니다.
프로젝트에 적용하기
1. install react-youtube
프로젝트에서 사용하는 패키지 매니저를 사용하여 설치해줍니다.
npm install react-youtube
or
yarn add react-youtube
or
pnpm add react-youtube
2. useZukebox
zukebox를 컨트롤 할 custom hook을 만들어줍니다.
import { playlist } from "@/data/playlist"
import { YouTubePlayer, YouTubeProps } from "react-youtube"
const PLAYLIST_ID = process.env.NEXT_PUBLIC_YOUTUBE_PLAYLIST_ID
export default function useZukebox() {
const [player, setPlayer] = useState<YouTubePlayer | null>(null)
const [isPlaying, setIsPlaying] = useState<boolean>(false)
const [volume, setVolume] = useState<number>(50)
const [currentTrack, setCurrentTrack] = useState<number>(0)
const opts: YouTubeProps["opts"] = {
height: "0",
width: "0",
playerVars: {
listType: "playlist",
list: PLAYLIST_ID,
autoplay: 1,
mute: 0,
loop: 1,
},
return {
isPlaying,
setIsPlaying,
volume,
setVolume,
currentTrack,
setCurrentTrack,
opts,
}
}
- PLAYLIST_ID : 만들어둔 유튜브 재생목록의 ID입니다.
https://www.youtube.com/playlist?list={PLAYLIST_ID}에 해당하는 ID를 환경변수에 저장합니다.
state
player
: YouTube Player객체입니다.isPlaying
: UI를 통해 재생 / 일시정지를 하기 위한 상태입니다.volume
: UI를 통해 BGM의 음량을 조절하기 위한 상태입니다.currentTrack
: 재생목록에서 현재 재생되는 트랙의 인덱스를 저장합니다.
opts
player
객체에서 사용할 옵션입니다.
영상 대신 노래만 재생되도록 높이와 너비를 0으로 설정해주었습니다.
listType
,list
: 속성 값이playlist
인 경우list
속성에 재생목록 ID를 지정합니다.autoplay
: 영상을 자동으로 재생할지에 대한 옵션입니다.
다만 특정 모바일 브라우저(예: Chrome 및 Safari)의 정책에 의해mute: 0
으로 설정되어있어야 소리없이 영상만 자동 재생됩니다.
이전에 접속하여 재생한 기록이 브라우저에 존재하는 경우, 다음 번 접속시 자동으로 재생되어 설정해주었습니다.mute
: 플레이어 음소거에 대한 옵션입니다.loop
: 재생목록 연속 재생에 대한 옵션입니다. 1로 설정해두어 재생목록의 마지막 동영상이 끝났을 때 목록의 첫번째 영상을 자동 로드하고 재생합니다.
모바일에선 loop, autoplay가 동작하지 않습니다.
관련 안내문은 이곳에서 확인할 수 있습니다.모바일 브라우저는 사용자에게 비용이 부과되는 원하지 않는 다운로드가 발생하는 것을 방지합니다.
때문에 모바일에선 자동으로 다음곡이 재생되지 않고, 사용자와의 상호작용이 있어야지만 재생됩니다.
event callback function
이벤트에 대한 콜백 함수들입니다.
onReady
이 이벤트는 플레이어 로드가 완료되고 API 호출을 받을 준비가 될 때마다 실행됩니다.
플레이어가 준비되는 즉시 특정 작업을 자동으로 실행하려면 애플리케이션에서 이 함수를 구현해야 합니다.
onReady
함수는 동영상 플레이어가 준비되면 음량을 설정해주었습니다.
const onReady = (event: { target: YouTubePlayer }) => {
event.target.setVolume(volume)
setPlayer(event.target)
}
onStateChange
이 이벤트는 플레이어의 상태가 변경될 때마다 실행됩니다.
API가 이벤트 리스너 함수에 전달하는 이벤트 개체의 data
속성의 값은 다음과 같습니다.
1
(시작되지 않음)0
(종료됨)1
(재생 중)2
(일시중지됨)3
(버퍼링 중)5
(동영상 신호)
플레이어의 재생 상태와 UI를 일치시켜주기 위해 상태가 변경되도록 작성했습니다.
const onStateChange = (event: { target: YouTubePlayer; data: number }) => {
//play
if (event.data === 1) {
setIsPlaying(true)
const currentIndex = event.target.getPlaylistIndex()
if (currentIndex !== currentTrack) setCurrentTrack(currentIndex)
}
//pause
if (event.data === 2) {
setIsPlaying(false)
}
}
handle function
UI를 통해 재생목록, 영상의 상태를 컨트롤하기 위한 함수들입니다.
handlePlayPause
특정(다수..) 브라우저의 정책에 의해 autoplay
가 동작하지 않기 때문에 영상을 재생 하려면 사용자와의 상호작용이 필수입니다. 사용자가 재생버튼을 눌렀을 때 실행할 함수를 만들어줍니다.
const handlePlayPause = () => {
if (player) {
if (isPlaying) {
player.pauseVideo()
} else {
player.playVideo()
}
setIsPlaying(!isPlaying)
}
}
handleNext
다음 노래(영상)을 재생하는 버튼을 눌렀을 때 발생할 이벤트의 함수입니다.
const handleNext = () => {
if (player) {
player.nextVideo()
setCurrentTrack((prev) => (prev + 1) % playlist.length)
if (!isPlaying) {
setIsPlaying(!isPlaying)
}
}
}
handlePrevious
이전 노래(영상) 재생에 대한 함수입니다.
const handlePrevious = () => {
if (player) {
player.previousVideo()
setCurrentTrack((prev) => (prev - 1 + playlist.length) % playlist.length)
if (!isPlaying) {
setIsPlaying(!isPlaying)
}
}
}
handleReplay
다시 재생에 대한 함수입니다.
const handleReplay = () => {
if (player) {
player.seekTo(0)
}
}
handleVolumeChange
볼륨 설정에 대한 함수입니다.
const handleVolumeChange = (event: Event, newValue: number | number[]) => {
if (player) {
const volumeValue = Array.isArray(newValue) ? newValue[0] : newValue
setVolume(volumeValue)
player.setVolume(volumeValue)
}
}
handleVideoAt
재생목록을 리스트 컴포넌트로 제공합니다. 사용자가 재생하고싶은 아이템을 클릭하면 해당 노래(영상)의 index를 재생하도록 하는 함수입니다.
const handleVideoAt = (index: number) => {
if (player) {
player.playVideoAt(index)
setCurrentTrack(index)
if (!isPlaying) {
setIsPlaying(!isPlaying)
}
}
}
3. Render Component
Zukebox
<YouTube>를 호출하여 이벤트에 만들어둔 함수와 opts
를 설정해줍니다.
import YouTube from "react-youtube"
export default function Zukebox() {
const { {/* .. */} } = useZukebox()
const { showPlayList, handleShowPlayList } = useShowPlaylist()
const { title, artist } = playlist[currentTrack]
return (
<GradientBox type="box">
<Box>
<IconButton onClick={handlePlayPause}>
{isPlaying ? ( <Pause /> ) : ( <PlayArrow /> )}
</IconButton>
<Box>
<CustomTypography>
{`${title} - ${artist}`}
</CustomTypography>
</Box>
<Tooltip title="Show Playlist">
<Image src={cd} onClick={handleShowPlayList} />
</Tooltip>
{/* YouTube 컴포넌트 */}
<YouTube
videoId=""
opts={opts}
onReady={onReady}
onStateChange={onStateChange}
style={{
display: "none",
}}
/>
{/* Zukebox UI 컨트롤 컴포넌트*/}
<ZukeboxControlButtons
handlePlayPause={handlePlayPause}
handlePrevious={handlePrevious}
handleNext={handleNext}
handleReplay={handleReplay}
handleVideoAt={handleVideoAt}
currentTrack={currentTrack}
isPlaying={isPlaying}
/>
</Box>
{/* 재생목록을 표시할 모달 컴포넌트 */}
<Playlist
open={showPlayList}
handleClose={handleShowPlayList}
/>
</GradientBox>
)
}
Playlist
재생목록을 표시할 모달 컴포넌트입니다.
handleVideoAt()
를 호출하여 재생할 노래(영상)을 선택할 수 있도록 해주었습니다.
export default function Playlist({
open,
handleClose,
}: PlaylistProps) {
const { currentTrack, handleVideoAt } = useZukebox()
return (
<Modal
open={open}
onClose={handleClose}
>
<Box
id="modal-content"
className="modal-description"
>
<Box>
<CustomTypography>
Time: {time}
</CustomTypography>
{playlist.map((info, index) => (
<Box key={index}>
<Button onClick={() => handleVideoAt(index)}>
<CustomTypography
color={
currentTrack === index
? "var(--primary)"
: "var(--text)"
}
>
{index}. {info.title} - {info.artist}
</CustomTypography>
<CustomTypography>
{info.time}
</CustomTypography>
</Button>
</Box>
))}
</Box>
<Button onClick={handleClose} >
닫기
</Button>
</Box>
</Modal>
)
}
4. 완성
컨셉에 맞게 UI를 꾸며주면 완성입니다.
YouTube API에서 현재 재생되는 영상의 title을 넘겨주지만, 대부분의 영상 제목이 title - artist
형식으로 지정되어있지 않기 때문에 data
파일에 객체를 만들어 추가적으로 관리해주었습니다.
블로그 컨셉의 핵심적인 기능..이라고 개인적으로 생각해 꼭 넣고 싶었던 피쳐입니다.
모바일에서 자동재생, 루프가 되지 않는 점이 불편하지만 GA로 확인해본 결과 Web 접속자 비율이 월등했기에.. 모른척 하고 있습니다.