Low Love - King Gnu

cd

obvoso

star

Youtube API로 Next.js 블로그에 BGM 만들기

react-youtube 라이브러리로 저렴한 Zukebox 만들기

개발
Blog
React
thumbnail

서론

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,
  }
}

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. 완성

14e3cd4b-ffcd-8096-9623-c4b642941532--0.png

컨셉에 맞게 UI를 꾸며주면 완성입니다.
YouTube API에서 현재 재생되는 영상의 title을 넘겨주지만, 대부분의 영상 제목이 title - artist 형식으로 지정되어있지 않기 때문에 data파일에 객체를 만들어 추가적으로 관리해주었습니다.

블로그 컨셉의 핵심적인 기능..이라고 개인적으로 생각해 꼭 넣고 싶었던 피쳐입니다.

모바일에서 자동재생, 루프가 되지 않는 점이 불편하지만 GA로 확인해본 결과 Web 접속자 비율이 월등했기에.. 모른척 하고 있습니다.

레퍼런스

star

이전 포스트

React의 fetch가 어떻게 Next.js의 request memoization에서 사용되는가

리액트에서 Web API인 fetch를 어떻게 확장했나

2024년 11월 23일