일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
Tags
- git remote upstream
- 타입스크립트
- eslint
- frontend
- aspect-ratio
- FRONT-END
- git remote -v
- aspect ratio
- git remote
- git prune
- flex-box
- CSS
- stash
- prettier
- 프론트엔드
- border-box
- content-box
- git remote origin
- developer
- 중간저장
- eslint-disable
- REACT
- git stash
- JavaScript
- 커밋
- git
- 유틸리티 타입
- 깃
- typeScript
- 상태관리
Archives
- Today
- Total
dev_story
[CSS] 유튜브 전체화면을 웹에서 구현하려면? 본문
반응형
웹에서 유튜브 스타일 전체화면 구현 완벽 가이드
📋 목차
🔍 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 특별 처리)
- 에러 처리 및 권한 거부 대응
- 성능 최적화 및 메모리 누수 방지
🚀 권장 사항
- useFullscreen 훅 활용으로 재사용성 높이기
- TypeScript 지원으로 타입 안정성 확보
- 적절한 UX 패턴 적용 (유튜브, 넷플릭스 참고)
- 철저한 테스트 (다양한 브라우저, 디바이스)
🔗 참고 자료
💡 Pro Tip: 전체화면 기능은 사용자 제스처(클릭, 키 입력 등) 이후에만 동작합니다. 페이지 로드 시 자동 실행은 불가능하니 주의하세요!
반응형
'CSS' 카테고리의 다른 글
[CSS] box-sizing이란? (0) | 2024.02.28 |
---|---|
[CSS] 이미지/동영상 비율을 임의로 설정하려면? - Aspect Ratio (2) | 2024.02.05 |
[CSS] width 100vw일 때 가로(좌우) 스크롤이 생긴다면? (2) | 2024.01.24 |