# 모바일 브라우저 Bottom 영역 처리 가이드 ## 개요 모바일 브라우저에서 발생하는 주요 문제들: - **동적 주소창**: 스크롤 시 주소창이 나타나고 사라지면서 viewport 높이가 변경됨 - **Safe Area**: iOS notch, Android 제스처 바 등 시스템 UI가 앱 컨텐츠를 가림 - **100vh 문제**: CSS `100vh`는 주소창을 포함한 높이로 계산되어 실제 보이는 영역보다 큼 이 가이드는 위 문제들을 해결하는 3가지 핵심 기법을 제공합니다. --- ## 🎯 핵심 기법 요약 | 기법 | 목적 | 브라우저 지원 | |------|------|--------------| | **1. visualViewport API** | 실시간 viewport 높이 추적 | iOS Safari 13+, Chrome 61+ | | **2. CSS dvh 단위** | 동적 viewport 높이 | iOS Safari 15.4+, Chrome 108+ | | **3. safe-area-inset** | 시스템 UI 회피 | iOS Safari 11+, Chrome 69+ | --- ## 📐 기법 1: JavaScript visualViewport API ### 원리 모바일 브라우저의 **실제 사용 가능한** viewport 높이를 JavaScript로 계산하여 CSS 변수에 저장합니다. ### 구현 코드 ```javascript // 앱 진입점 (예: entry-client.tsx, index.js) const setAppHeight = () => { // visualViewport API로 정확한 높이 측정 const vh = window.visualViewport?.height ?? window.innerHeight; // CSS 변수 업데이트 document.documentElement.style.setProperty('--vh', `${vh * 0.01}px`); document.documentElement.style.setProperty('--app-height', `${vh}px`); }; // 초기 설정 setAppHeight(); // 이벤트 리스너 등록 window.addEventListener('resize', setAppHeight); window.addEventListener('orientationchange', setAppHeight); // visualViewport API 지원 브라우저 if (window.visualViewport) { window.visualViewport.addEventListener('resize', setAppHeight); window.visualViewport.addEventListener('scroll', setAppHeight); } ``` ### CSS에서 사용 ```css /* 초기값 설정 (JavaScript 로드 전 fallback) */ :root { --vh: 1vh; --app-height: 100vh; } /* 앱 최상위 요소에 적용 */ #root { height: var(--app-height); overflow: hidden; } ``` ### 장점 - 가장 정확한 높이 계산 - 주소창 표시/숨김 실시간 대응 - 모든 모바일 브라우저에서 동작 (fallback 포함) --- ## 🎨 기법 2: CSS dvh 단위 + Fallback 전략 ### 원리 CSS의 새로운 viewport 단위인 `dvh` (Dynamic Viewport Height)를 사용하고, 구형 브라우저를 위한 fallback을 제공합니다. ### 3단계 Cascade 전략 ```css #root { /* 1단계: 구형 브라우저 fallback */ height: 100vh; /* 2단계: dvh 지원 브라우저 (CSS만으로 해결) */ height: 100dvh; /* 3단계: JavaScript 계산값 (최우선) */ height: var(--app-height); overflow: hidden; } ``` ### dvh vs vh 비교 | 단위 | 설명 | 예시 | |------|------|------| | `vh` | **정적** viewport 높이 (주소창 포함) | 주소창이 숨겨져도 높이 고정 | | `dvh` | **동적** viewport 높이 (주소창 제외) | 주소창 표시/숨김에 따라 자동 조정 | | `svh` | **소형** viewport 높이 (주소창 표시 상태) | 가장 작은 높이 | | `lvh` | **대형** viewport 높이 (주소창 숨김 상태) | 가장 큰 높이 | ### React/Vue 컴포넌트에서 사용 ```jsx // React 예시 function App() { return (
{/* 앱 컨텐츠 */}
); } ``` --- ## 🛡️ 기법 3: Safe Area Inset 처리 ### 원리 iOS notch, Android 제스처 바 등 시스템 UI 영역을 `env()` 함수로 감지하여 UI 요소가 가려지지 않도록 합니다. ### 1단계: viewport-fit 설정 ```html ``` `viewport-fit=cover`: safe-area-inset 환경 변수를 활성화합니다. ### 2단계: 하단 UI 요소 보호 #### Tailwind CSS 사용 예시 ```jsx // 하단 고정 버튼 // 하단 네비게이션 바 // 하단에서 떠있는 Toast
알림 메시지
``` #### 일반 CSS 사용 예시 ```css .bottom-button { position: fixed; bottom: 0; width: 100%; /* 최소 1rem, 시스템 UI 영역이 더 크면 그 값 사용 */ padding-bottom: max(1rem, env(safe-area-inset-bottom)); } .bottom-nav { position: fixed; bottom: 0; width: 100%; padding-bottom: env(safe-area-inset-bottom, 0); /* 두 번째 인자는 fallback 값 (구형 브라우저) */ } .floating-toast { position: fixed; right: 1rem; /* safe-area + 추가 여백 */ bottom: calc(env(safe-area-inset-bottom, 0) + 4rem); } ``` ### 모든 Safe Area Inset 변수 ```css /* 모든 방향의 safe area */ padding-top: env(safe-area-inset-top); padding-right: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); /* 단축 속성 */ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); ``` --- ## 🔧 통합 적용 가이드 ### Step 1: HTML 설정 ```html Your App
``` ### Step 2: CSS 기본 설정 ```css /* global.css or index.css */ :root { /* JavaScript fallback */ --vh: 1vh; --app-height: 100vh; } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; } #root { width: 100%; height: 100vh; /* fallback */ height: 100dvh; /* modern browsers */ height: var(--app-height); /* JS-calculated */ overflow: hidden; } ``` ### Step 3: JavaScript 초기화 ```javascript // main.js, entry-client.tsx 등 function initViewportHeight() { const setAppHeight = () => { const vh = window.visualViewport?.height ?? window.innerHeight; document.documentElement.style.setProperty('--vh', `${vh * 0.01}px`); document.documentElement.style.setProperty('--app-height', `${vh}px`); }; setAppHeight(); window.addEventListener('resize', setAppHeight); window.addEventListener('orientationchange', setAppHeight); if (window.visualViewport) { window.visualViewport.addEventListener('resize', setAppHeight); window.visualViewport.addEventListener('scroll', setAppHeight); } } // 앱 시작 시 즉시 실행 initViewportHeight(); ``` ### Step 4: 컴포넌트 적용 예시 ```jsx // App.jsx / App.tsx export default function App() { return (
{/* Header */}
헤더
{/* Main Content (스크롤 가능) */}
컨텐츠
{/* Bottom Navigation (Safe Area 적용) */}
); } ``` --- ## 📱 실전 예시: 다양한 Bottom UI 패턴 ### 1. 고정 하단 버튼 ```jsx ``` ### 2. Floating Action Button (FAB) ```jsx ``` ### 3. Bottom Sheet ```jsx
Bottom Sheet 내용
``` ### 4. Sticky Footer with Content ```jsx

총 금액: 50,000원

``` --- ## 🌐 브라우저 호환성 ### visualViewport API - ✅ iOS Safari 13+ - ✅ Chrome 61+ - ✅ Firefox 91+ - ✅ Samsung Internet 8.0+ - ❌ IE (fallback 필요) ### CSS dvh 단위 - ✅ iOS Safari 15.4+ - ✅ Chrome 108+ - ✅ Firefox 110+ - ✅ Samsung Internet 21+ ### safe-area-inset - ✅ iOS Safari 11.0+ (notch 대응) - ✅ Chrome 69+ (Android) - ✅ Samsung Internet 10.0+ --- ## ⚠️ 주의사항 및 팁 ### 1. 성능 최적화 ```javascript // Throttle 적용 (선택사항) let timeoutId; const setAppHeight = () => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { const vh = window.visualViewport?.height ?? window.innerHeight; document.documentElement.style.setProperty('--app-height', `${vh}px`); }, 100); }; ``` ### 2. SSR/SSG 환경 ```javascript // 서버 사이드에서 안전하게 처리 if (typeof window !== 'undefined') { initViewportHeight(); } ``` ### 3. iOS Safari 특이사항 - iOS Safari는 주소창이 사라질 때 `resize` 이벤트가 발생하지 않을 수 있음 - `visualViewport` 이벤트를 반드시 함께 사용할 것 - `orientationchange` 이벤트도 필수 ### 4. 테스트 방법 ```javascript // 개발자 도구에서 현재 값 확인 console.log('vh:', getComputedStyle(document.documentElement).getPropertyValue('--vh')); console.log('app-height:', getComputedStyle(document.documentElement).getPropertyValue('--app-height')); console.log('safe-area-bottom:', getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)')); ``` --- ## 🎁 Bonus: TypeScript 타입 정의 ```typescript // global.d.ts interface Window { visualViewport?: { height: number; width: number; scale: number; addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; }; } // React 컴포넌트에서 사용 const AppHeightProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { useEffect(() => { const setAppHeight = (): void => { const vh = window.visualViewport?.height ?? window.innerHeight; document.documentElement.style.setProperty('--app-height', `${vh}px`); }; setAppHeight(); window.addEventListener('resize', setAppHeight); window.visualViewport?.addEventListener('resize', setAppHeight); return () => { window.removeEventListener('resize', setAppHeight); window.visualViewport?.removeEventListener('resize', setAppHeight); }; }, []); return <>{children}; }; ``` --- ## 📚 참고 자료 - [MDN - Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) - [MDN - CSS env()](https://developer.mozilla.org/en-US/docs/Web/CSS/env) - [W3C - CSS Values and Units Level 4 (dvh)](https://www.w3.org/TR/css-values-4/#viewport-relative-lengths) - [WebKit Blog - Designing Websites for iPhone X](https://webkit.org/blog/7929/designing-websites-for-iphone-x/) --- ## ✅ 체크리스트 적용 완료 확인: - [ ] HTML에 `viewport-fit=cover` 추가 - [ ] CSS에 `:root` 변수 및 `#root` 높이 cascade 설정 - [ ] JavaScript viewport 높이 계산 및 이벤트 리스너 등록 - [ ] 하단 UI 요소에 `safe-area-inset-bottom` 적용 - [ ] 모바일 실기기에서 테스트 (iOS Safari, Chrome) - [ ] 화면 회전 시 정상 동작 확인 - [ ] 주소창 스크롤 시 레이아웃 깨짐 없음 확인 --- **작성일**: 2025-11-20 **적용 프로젝트**: HunyDev (huny.dev) **버전**: 1.0