# 모바일 브라우저 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 적용) */}