dev_story

[CSS] 유튜브 전체화면을 웹에서 구현하려면? 본문

CSS

[CSS] 유튜브 전체화면을 웹에서 구현하려면?

dev_sihyun 2024. 2. 1. 17:40
반응형

웹에서 유튜브 스타일 전체화면 구현 완벽 가이드

Full Screen Demo

📋 목차


🔍 Fullscreen API 이해하기

전체화면 모드의 이점

전체화면 기능은 다음과 같은 상황에서 사용자 경험을 크게 향상시킵니다:

  • 🎥 미디어 재생: 동영상, 이미지 갤러리
  • 🎮 게임 애플리케이션: 몰입감 증대
  • 📊 데이터 시각화: 대시보드, 차트
  • 📝 집중 모드: 에디터, 프레젠테이션

Fullscreen API 핵심 메서드

// 전체화면 진입
element.requestFullscreen()

// 전체화면 종료
document.exitFullscreen()

// 전체화면 상태 확인
document.fullscreenElement

💻 기본 구현 방법

1. 개선된 React 컴포넌트

import React, { useState, useEffect } from "react";
import { Maximize, Minimize } from "lucide-react"; // 아이콘 라이브러리

const FullScreenButton = ({ targetElement = null }) => {
  const [isFullscreen, setIsFullscreen] = useState(false);

  // 전체화면 상태 감지
  useEffect(() => {
    const handleFullscreenChange = () => {
      setIsFullscreen(!!document.fullscreenElement);
    };

    document.addEventListener('fullscreenchange', handleFullscreenChange);
    document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
    document.addEventListener('mozfullscreenchange', handleFullscreenChange);
    document.addEventListener('MSFullscreenChange', handleFullscreenChange);

    return () => {
      document.removeEventListener('fullscreenchange', handleFullscreenChange);
      document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
      document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
      document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
    };
  }, []);

  const toggleFullscreen = async () => {
    try {
      const element = targetElement || document.documentElement;

      if (isFullscreen) {
        // 전체화면 종료
        if (document.exitFullscreen) {
          await document.exitFullscreen();
        } else if (document.webkitExitFullscreen) {
          await document.webkitExitFullscreen();
        } else if (document.mozCancelFullScreen) {
          await document.mozCancelFullScreen();
        } else if (document.msExitFullscreen) {
          await document.msExitFullscreen();
        }
      } else {
        // 전체화면 진입
        if (element.requestFullscreen) {
          await element.requestFullscreen();
        } else if (element.webkitRequestFullscreen) {
          await element.webkitRequestFullscreen();
        } else if (element.mozRequestFullScreen) {
          await element.mozRequestFullScreen();
        } else if (element.msRequestFullscreen) {
          await element.msRequestFullscreen();
        }
      }
    } catch (error) {
      console.error('전체화면 전환 중 오류:', error);
    }
  };

  return (
    <button 
      className="fullscreen-button"
      onClick={toggleFullscreen}
      title={isFullscreen ? "전체화면 종료 (ESC)" : "전체화면 (F11)"}
      aria-label={isFullscreen ? "전체화면 종료" : "전체화면 진입"}
    >
      {isFullscreen ? (
        <Minimize size={24} />
      ) : (
        <Maximize size={24} />
      )}
    </button>
  );
};

export default FullScreenButton;

2. 개선된 CSS 스타일

/* 전체화면 버튼 스타일 */
.fullscreen-button {
  /* 기본 스타일 */
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;

  /* 버튼 디자인 */
  width: 48px;
  height: 48px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.7);
  color: white;

  /* 중앙 정렬 */
  display: flex;
  align-items: center;
  justify-content: center;

  /* 인터랙션 */
  cursor: pointer;
  transition: all 0.3s ease;
  backdrop-filter: blur(4px);
}

/* 호버 효과 */
.fullscreen-button:hover {
  background: rgba(0, 0, 0, 0.9);
  transform: scale(1.1);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

/* 포커스 접근성 */
.fullscreen-button:focus {
  outline: 2px solid #007acc;
  outline-offset: 2px;
}

/* 전체화면 시 특별 스타일 */
:fullscreen .fullscreen-button,
:-webkit-full-screen .fullscreen-button,
:-moz-full-screen .fullscreen-button {
  background: rgba(255, 255, 255, 0.2);
  border: 1px solid rgba(255, 255, 255, 0.3);
}

/* 모바일 대응 */
@media (max-width: 768px) {
  .fullscreen-button {
    width: 44px;
    height: 44px;
    bottom: 16px;
    right: 16px;
  }
}

/* 전체화면 모드 시 컨텐츠 스타일 */
.fullscreen-container {
  transition: all 0.3s ease;
}

.fullscreen-container:fullscreen,
.fullscreen-container:-webkit-full-screen,
.fullscreen-container:-moz-full-screen {
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}

🚀 고급 구현 및 최적화

1. 커스텀 훅으로 재사용성 높이기

import { useState, useEffect, useCallback } from 'react';

const useFullscreen = (targetRef = null) => {
  const [isFullscreen, setIsFullscreen] = useState(false);

  // 전체화면 상태 확인
  const checkFullscreen = useCallback(() => {
    const fullscreenElement = 
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement ||
      document.msFullscreenElement;

    setIsFullscreen(!!fullscreenElement);
    return !!fullscreenElement;
  }, []);

  // 전체화면 진입
  const enterFullscreen = useCallback(async (element) => {
    try {
      const target = element || targetRef?.current || document.documentElement;

      if (target.requestFullscreen) {
        await target.requestFullscreen();
      } else if (target.webkitRequestFullscreen) {
        await target.webkitRequestFullscreen();
      } else if (target.mozRequestFullScreen) {
        await target.mozRequestFullScreen();
      } else if (target.msRequestFullscreen) {
        await target.msRequestFullscreen();
      }
    } catch (error) {
      console.error('전체화면 진입 실패:', error);
      throw error;
    }
  }, [targetRef]);

  // 전체화면 종료
  const exitFullscreen = useCallback(async () => {
    try {
      if (document.exitFullscreen) {
        await document.exitFullscreen();
      } else if (document.webkitExitFullscreen) {
        await document.webkitExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        await document.mozCancelFullScreen();
      } else if (document.msExitFullscreen) {
        await document.msExitFullscreen();
      }
    } catch (error) {
      console.error('전체화면 종료 실패:', error);
      throw error;
    }
  }, []);

  // 전체화면 토글
  const toggleFullscreen = useCallback(async (element) => {
    if (isFullscreen) {
      await exitFullscreen();
    } else {
      await enterFullscreen(element);
    }
  }, [isFullscreen, enterFullscreen, exitFullscreen]);

  // 이벤트 리스너 등록
  useEffect(() => {
    const events = [
      'fullscreenchange',
      'webkitfullscreenchange',
      'mozfullscreenchange',
      'MSFullscreenChange'
    ];

    events.forEach(event => {
      document.addEventListener(event, checkFullscreen);
    });

    // 초기 상태 확인
    checkFullscreen();

    return () => {
      events.forEach(event => {
        document.removeEventListener(event, checkFullscreen);
      });
    };
  }, [checkFullscreen]);

  return {
    isFullscreen,
    enterFullscreen,
    exitFullscreen,
    toggleFullscreen
  };
};

// 사용 예시
const VideoPlayer = () => {
  const videoRef = useRef(null);
  const { isFullscreen, toggleFullscreen } = useFullscreen(videoRef);

  return (
    <div ref={videoRef} className="video-container">
      <video src="video.mp4" controls />
      <button onClick={() => toggleFullscreen()}>
        {isFullscreen ? '전체화면 종료' : '전체화면'}
      </button>
    </div>
  );
};

2. TypeScript 지원 버전

interface FullscreenElement extends Element {
  requestFullscreen?: () => Promise<void>;
  webkitRequestFullscreen?: () => Promise<void>;
  mozRequestFullScreen?: () => Promise<void>;
  msRequestFullscreen?: () => Promise<void>;
}

interface FullscreenDocument extends Document {
  exitFullscreen?: () => Promise<void>;
  webkitExitFullscreen?: () => Promise<void>;
  mozCancelFullScreen?: () => Promise<void>;
  msExitFullscreen?: () => Promise<void>;
  fullscreenElement?: Element;
  webkitFullscreenElement?: Element;
  mozFullScreenElement?: Element;
  msFullscreenElement?: Element;
}

const useFullscreen = (targetRef: RefObject<FullscreenElement> = null) => {
  // ... 위와 동일한 로직
};

🌐 브라우저 호환성 처리

지원 현황 확인

const checkFullscreenSupport = () => {
  const support = {
    standard: 'requestFullscreen' in document.documentElement,
    webkit: 'webkitRequestFullscreen' in document.documentElement,
    moz: 'mozRequestFullScreen' in document.documentElement,
    ms: 'msRequestFullscreen' in document.documentElement
  };

  return {
    ...support,
    isSupported: Object.values(support).some(Boolean)
  };
};

// 사용 예시
const FullscreenComponent = () => {
  const [supportInfo, setSupportInfo] = useState(null);

  useEffect(() => {
    setSupportInfo(checkFullscreenSupport());
  }, []);

  if (!supportInfo?.isSupported) {
    return <div>전체화면을 지원하지 않는 브라우저입니다.</div>;
  }

  return (
    // 전체화면 컴포넌트
  );
};

Polyfill 구현

// 간단한 폴리필
if (!Element.prototype.requestFullscreen) {
  Element.prototype.requestFullscreen = 
    Element.prototype.webkitRequestFullscreen ||
    Element.prototype.mozRequestFullScreen ||
    Element.prototype.msRequestFullscreen ||
    function() {
      console.warn('전체화면 API를 지원하지 않습니다.');
      return Promise.resolve();
    };
}

if (!Document.prototype.exitFullscreen) {
  Document.prototype.exitFullscreen = 
    Document.prototype.webkitExitFullscreen ||
    Document.prototype.mozCancelFullScreen ||
    Document.prototype.msExitFullscreen ||
    function() {
      console.warn('전체화면 종료 API를 지원하지 않습니다.');
      return Promise.resolve();
    };
}

🎯 실무 활용 예제

1. 이미지 갤러리 전체화면

const ImageGallery = ({ images }) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const galleryRef = useRef(null);
  const { isFullscreen, toggleFullscreen } = useFullscreen(galleryRef);

  const handleKeyPress = useCallback((event) => {
    if (!isFullscreen) return;

    switch (event.key) {
      case 'ArrowLeft':
        setCurrentIndex(prev => Math.max(0, prev - 1));
        break;
      case 'ArrowRight':
        setCurrentIndex(prev => Math.min(images.length - 1, prev + 1));
        break;
      case 'Escape':
        // ESC는 브라우저가 자동 처리
        break;
    }
  }, [isFullscreen, images.length]);

  useEffect(() => {
    if (isFullscreen) {
      document.addEventListener('keydown', handleKeyPress);
    }

    return () => {
      document.removeEventListener('keydown', handleKeyPress);
    };
  }, [isFullscreen, handleKeyPress]);

  return (
    <div ref={galleryRef} className={`gallery ${isFullscreen ? 'fullscreen' : ''}`}>
      <img 
        src={images[currentIndex]} 
        alt={`Image ${currentIndex + 1}`}
        className="gallery-image"
      />

      <div className="gallery-controls">
        <button onClick={() => setCurrentIndex(prev => Math.max(0, prev - 1))}>
          이전
        </button>
        <button onClick={toggleFullscreen}>
          {isFullscreen ? '전체화면 종료' : '전체화면'}
        </button>
        <button onClick={() => setCurrentIndex(prev => Math.min(images.length - 1, prev + 1))}>
          다음
        </button>
      </div>
    </div>
  );
};

2. 비디오 플레이어 구현

const VideoPlayer = ({ src, poster }) => {
  const videoRef = useRef(null);
  const containerRef = useRef(null);
  const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
  const [isPlaying, setIsPlaying] = useState(false);

  const handleVideoClick = () => {
    if (videoRef.current) {
      if (isPlaying) {
        videoRef.current.pause();
      } else {
        videoRef.current.play();
      }
      setIsPlaying(!isPlaying);
    }
  };

  return (
    <div ref={containerRef} className="video-player-container">
      <video
        ref={videoRef}
        src={src}
        poster={poster}
        onClick={handleVideoClick}
        className="video-element"
      />

      <div className="video-controls">
        <button onClick={handleVideoClick}>
          {isPlaying ? '일시정지' : '재생'}
        </button>
        <button onClick={toggleFullscreen}>
          {isFullscreen ? '전체화면 종료' : '전체화면'}
        </button>
      </div>

      {/* 전체화면 시에만 표시되는 컨트롤 */}
      {isFullscreen && (
        <div className="fullscreen-overlay">
          <div className="fullscreen-info">
            ESC를 눌러 전체화면을 종료하세요
          </div>
        </div>
      )}
    </div>
  );
};

🎨 UX 개선 팁

1. 로딩 인디케이터

const FullscreenButton = () => {
  const [isLoading, setIsLoading] = useState(false);
  const { isFullscreen, toggleFullscreen } = useFullscreen();

  const handleToggle = async () => {
    setIsLoading(true);
    try {
      await toggleFullscreen();
    } catch (error) {
      console.error('전체화면 전환 실패:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button 
      className={`fullscreen-btn ${isLoading ? 'loading' : ''}`}
      onClick={handleToggle}
      disabled={isLoading}
    >
      {isLoading ? (
        <div className="spinner" />
      ) : isFullscreen ? (
        <ExitFullscreenIcon />
      ) : (
        <FullscreenIcon />
      )}
    </button>
  );
};

2. 애니메이션 효과

/* 부드러운 전환 효과 */
.fullscreen-container {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.fullscreen-container:fullscreen {
  animation: fullscreen-enter 0.3s ease-out;
}

@keyframes fullscreen-enter {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* 버튼 애니메이션 */
.fullscreen-btn {
  position: relative;
  overflow: hidden;
}

.fullscreen-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  transition: width 0.6s, height 0.6s;
}

.fullscreen-btn:active::before {
  width: 300px;
  height: 300px;
}

3. 키보드 단축키 안내

const KeyboardShortcuts = ({ isVisible }) => {
  if (!isVisible) return null;

  return (
    <div className="keyboard-shortcuts">
      <h3>키보드 단축키</h3>
      <ul>
        <li><kbd>F11</kbd> - 전체화면 토글</li>
        <li><kbd>ESC</kbd> - 전체화면 종료</li>
        <li><kbd>Space</kbd> - 재생/일시정지</li>
        <li><kbd>←/→</kbd> - 이전/다음</li>
      </ul>
    </div>
  );
};

🔧 문제 해결

자주 발생하는 오류들

1. "API can only be initiated by a user gesture" 오류

// ❌ 잘못된 방법 - 자동 실행
useEffect(() => {
  toggleFullscreen(); // 오류 발생!
}, []);

// ✅ 올바른 방법 - 사용자 인터랙션 후 실행
const handleButtonClick = () => {
  toggleFullscreen(); // 정상 동작
};

2. iOS Safari 전체화면 문제

// iOS Safari용 특별 처리
const handleFullscreenIOS = () => {
  if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
    // iOS에서는 video 요소에만 전체화면 적용 가능
    const video = document.querySelector('video');
    if (video && video.webkitEnterFullscreen) {
      video.webkitEnterFullscreen();
    }
  } else {
    toggleFullscreen();
  }
};

3. 권한 거부 처리

const safeToggleFullscreen = async () => {
  try {
    await toggleFullscreen();
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      alert('전체화면 권한이 거부되었습니다.');
    } else if (error.name === 'TypeError') {
      alert('이 브라우저에서는 전체화면을 지원하지 않습니다.');
    } else {
      console.error('전체화면 오류:', error);
    }
  }
};

디버깅 도구

const FullscreenDebugger = () => {
  const [debugInfo, setDebugInfo] = useState({});

  useEffect(() => {
    const updateDebugInfo = () => {
      setDebugInfo({
        isFullscreen: !!document.fullscreenElement,
        fullscreenElement: document.fullscreenElement?.tagName,
        screenSize: `${screen.width}x${screen.height}`,
        windowSize: `${window.innerWidth}x${window.innerHeight}`,
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString()
      });
    };

    const events = ['fullscreenchange', 'resize'];
    events.forEach(event => {
      document.addEventListener(event, updateDebugInfo);
    });

    updateDebugInfo();

    return () => {
      events.forEach(event => {
        document.removeEventListener(event, updateDebugInfo);
      });
    };
  }, []);

  return (
    <div className="debug-panel">
      <h4>전체화면 디버그 정보</h4>
      <pre>{JSON.stringify(debugInfo, null, 2)}</pre>
    </div>
  );
};

📊 성능 최적화

메모리 누수 방지

const useFullscreenOptimized = (targetRef) => {
  const { isFullscreen, toggleFullscreen } = useFullscreen(targetRef);

  // 컴포넌트 언마운트 시 전체화면 해제
  useEffect(() => {
    return () => {
      if (document.fullscreenElement) {
        document.exitFullscreen().catch(console.error);
      }
    };
  }, []);

  return { isFullscreen, toggleFullscreen };
};

배치(batch) 상태 업데이트

import { unstable_batchedUpdates } from 'react-dom';

const handleFullscreenChange = () => {
  unstable_batchedUpdates(() => {
    setIsFullscreen(!!document.fullscreenElement);
    setLastUpdated(Date.now());
    // 여러 상태 업데이트를 한 번에 처리
  });
};

🎉 마무리

웹에서 전체화면 기능을 구현할 때는 다음 사항들을 고려해야 합니다:

📝 체크리스트

  • 브라우저 호환성 확인 및 폴백 제공
  • 사용자 경험 개선 (로딩, 애니메이션, 키보드 지원)
  • 접근성 고려 (스크린 리더, 키보드 내비게이션)
  • 모바일 대응 (iOS Safari 특별 처리)
  • 에러 처리 및 권한 거부 대응
  • 성능 최적화 및 메모리 누수 방지

🚀 권장 사항

  1. useFullscreen 훅 활용으로 재사용성 높이기
  2. TypeScript 지원으로 타입 안정성 확보
  3. 적절한 UX 패턴 적용 (유튜브, 넷플릭스 참고)
  4. 철저한 테스트 (다양한 브라우저, 디바이스)

🔗 참고 자료


💡 Pro Tip: 전체화면 기능은 사용자 제스처(클릭, 키 입력 등) 이후에만 동작합니다. 페이지 로드 시 자동 실행은 불가능하니 주의하세요!

반응형