기능 | 배경음악(useVideoCheck hook) - ssseok/wedding.invitation GitHub Wiki

목적

  • 웹 사이트에 배경음악을 담당합니다.

설치

bunx --bun shadcn@latest add button

[배경음악]코드

import { Button } from '@/common/components/ui/button';
import { Volume2, VolumeX, Play } from 'lucide-react';
import { useRef, useState, useEffect } from 'react';

interface BackgroundMusicProps {
  hasVideo: boolean;
}

export default function BackgroundMusic({ hasVideo }: BackgroundMusicProps) {
  const [isPlaying, setIsPlaying] = useState<boolean>(false);
  const [isMuted, setIsMuted] = useState<boolean>(false);
  const [hasUserInteraction, setHasUserInteraction] = useState<boolean>(false);
  const audioRef = useRef<HTMLAudioElement>(null);

  useEffect(() => {
    const savedInteraction = localStorage.getItem('hasUserInteraction');
    if (savedInteraction === 'true') {
      setHasUserInteraction(true);
    }
  }, [hasUserInteraction, hasVideo]);

  useEffect(() => {
    if (!audioRef.current) return;

    if (hasVideo) {
      audioRef.current.pause();
      setIsPlaying(false);
    } else {
      audioRef.current.muted = true;
      setIsMuted(true);

      const playPromise = audioRef.current.play();

      if (playPromise !== undefined) {
        playPromise
          .then(() => {
            setIsPlaying(true);
            console.log('음소거 상태로 자동 재생 성공');

            if (hasUserInteraction) {
              audioRef.current!.muted = false;
              setIsMuted(false);
            }
          })
          .catch((error) => {
            console.error('자동 재생 실패:', error);
            setIsPlaying(false);
          });
      }
    }
  }, [hasVideo, hasUserInteraction]);

  const handleInitialPlay = () => {
    setHasUserInteraction(true);
    localStorage.setItem('hasUserInteraction', 'true');

    if (audioRef.current) {
      audioRef.current.muted = false;
      setIsMuted(false);

      const playPromise = audioRef.current.play();

      if (playPromise !== undefined) {
        playPromise
          .then(() => {
            setIsPlaying(true);
          })
          .catch((error) => {
            console.error('재생 실패:', error);
            setIsPlaying(false);
          });
      }
    }
  };

  const toggleAudio = () => {
    if (!audioRef.current) return;

    if (isPlaying) {
      audioRef.current.pause();
      setIsPlaying(false);
    } else {
      audioRef.current.muted = false;
      setIsMuted(false);

      const playPromise = audioRef.current.play();

      if (playPromise !== undefined) {
        playPromise
          .then(() => {
            setIsPlaying(true);
          })
          .catch((error) => {
            console.error('재생 실패:', error);
            setIsPlaying(false);
          });
      }
    }
  };

  return (
    <>
      <div className='fixed top-0 left-0 right-0 z-40'>
        <div className='max-w-screen-sm mx-auto relative'>
          <div className='absolute top-4 right-4'>
            {!hasUserInteraction ? (
              <Button
                variant='secondary'
                size='sm'
                className='rounded-full bg-white/80 hover:bg-white/90 shadow-sm animate-pulse z-50'
                onClick={handleInitialPlay}
              >
                <Play className='w-4 h-4' />
              </Button>
            ) : (
              <Button
                variant='secondary'
                size='sm'
                className='rounded-full bg-white/80 hover:bg-white/90 shadow-sm'
                onClick={toggleAudio}
              >
                {isPlaying && !isMuted ? (
                  <Volume2 className='w-4 h-4' />
                ) : (
                  <VolumeX className='w-4 h-4' />
                )}
              </Button>
            )}
          </div>
        </div>
      </div>

      <audio ref={audioRef} loop preload='auto' src='/music/here-with-me.mp3' />
    </>
  );
}
  • 상태 관리
    • hasVideo => 페이지에 비디오가 존재하는지 여부를 나타내는 boolean 값입니다. 비디오가 있을 경우, 배경 음악을 자동으로 중지하는 데 사용됩니다.(useVideoCheck hook 에서 가져온 값)
    • isPlaying => 오디오가 현재 재생 중인지 여부를 나타냅니다.
    • isMuted => 오디오가 음소거 상태인지 여부를 나타냅니다.
    • hasUserInteraction => 사용자가 음악 재생에 상호작용했는지 여부를 추적합니다. 브라우저의 자동 재생 제한을 우회하는 데 중요합니다.
    • audioRef => 오디오 요소에 대한 참조를 저장하는 ref입니다.
  • 사용자 상호작용 상태 확인하는 법
    • localstorage로 이전에 사용자가 음악 재생에 동의했는지 localStorage에서 확인합니다.
    • 이전에 사용자가 음악 재생에 동의했는지 localStorage에서 확인합니다.
    • 페이지 새로고침이나 재방문 시에도 사용자의 상태를 기억합니다.
  • 비디오 존재 여부에 따른 오디오 처리
    • 비디오(mp4 확장자)가 존재하면 비디오를 보여주게 되고, 비디오가 없으면 배너 사진을 보여줍니다.
  • handleInitialPlay (초기 재생 버튼 핸들러 함수)
    • 처음 웹 사이트에 접속하면, localstorage에는 아무것도 없기에 재생 버튼이 있습니다.
    • 재생 버튼을 누르면 localstoragehasUserInteraction 값이 true로 저장됩니다.
  • toggleAudio (재생/일시정지 토글 함수)
    • 현재 재생 상태에 따라 오디오를 일시 정지하거나 재생합니다.

[useVideoCheck] 비디오 URL이 유효한지 확인하고 비디오 존재 여부를 판단하는 코드

import { useState, useEffect } from 'react';

export function useVideoCheck(videoUrl: string) {
  const [hasVideo, setHasVideo] = useState<boolean>(false);
  const [isChecking, setIsChecking] = useState<boolean>(true);

  useEffect(() => {
    const checkVideo = async () => {
      try {
        // HEAD 요청 대신 실제 비디오 요소로 체크
        const video = document.createElement('video');
        video.src = videoUrl;

        // 비디오 로드 이벤트 처리
        video.onloadeddata = () => {
          setHasVideo(true);
          setIsChecking(false);
        };

        // 에러 처리
        video.onerror = () => {
          setHasVideo(false);
          setIsChecking(false);
        };
      } catch (error) {
        console.error('Video check error:', error);
        setHasVideo(false);
        setIsChecking(false);
      }
    };

    checkVideo();
  }, [videoUrl]);

  return { hasVideo, isChecking };
}
  • 상태 관리
    • hasVideo => 비디오 존재 여부를 나타내는 boolean 값
    • isChecking => 확인 작업 진행 중인지 여부를 나타내는 boolean 값 (즉 로딩이랑 똑같다고 보면됩니다.)
  • 동작
    • onloadeddata 이벤트 핸들러를 통해 비디오가 성공적으로 로드되었는지 확인합니다.
    • onerror 로 예외처리 해줍니다.
    • 비디오가 존재하면 hasVideotrue, isChecking 확인 작업중이면(로딩) true
⚠️ **GitHub.com Fallback** ⚠️