들어가며
이 블로그(yerim.dev)는 PWA로 설치해서 아이폰에서도 사용할 수 있도록 만들었습니다. 그런데 아이폰에서 열어보니, 하단 고정 네비게이션 바가 홈 인디케이터(제스처 바) 영역에 딱 붙어 있는 문제가 생겼습니다.
일반 Safari 브라우저에서는 브라우저 자체 UI가 safe area를 처리해주지만, PWA(standalone) 모드에서는 개발자가 직접 처리해야 한다는 걸 이때 처음 알게 됐습니다.
이 글에서는 문제를 어떻게 발견했는지, 어떤 방법들을 시도했는지, 그리고 최종적으로 어떤 방법을 선택했는지 그 과정을 정리했습니다.
개선할 점이나 다른 접근 방식이 있다면 댓글로 공유해 주세요! 🙌
문제 상황
iPhone X 이후 기기들은 화면 하단에 홈 인디케이터(얇은 가로 바)가 있습니다. 이 영역은 약 34px로, 이 위에 버튼이나 네비게이션이 올라오면 사용자가 실수로 터치하거나 시각적으로 가려지는 문제가 발생합니다.
PWA standalone 모드에서는 브라우저 UI가 없기 때문에, 이 영역을 개발자가 명시적으로 피해줘야 합니다.

시도한 방법들
방법 1. 고정 패딩
.bottom-nav {
padding-bottom: 34px;
}
가장 단순한 방법이지만 몇 가지 문제가 있었습니다.
- iPhone X 계열은 34px이지만, 홈 버튼이 있는 iPhone SE나 안드로이드는 0px
- 기기마다 값이 달라 하드코딩하면 불필요한 여백이 생기거나 부족할 수 있음
- 새로운 기기가 나올 때마다 별도로 대응해야 함
방법 2. JavaScript로 safe area 감지
const safeAreaBottom = window.screen.height - window.innerHeight
또는 visualViewport API를 활용하는 방식입니다.
- JS로 감지한 값을 CSS 변수에 주입하는 패턴
window크기 변화 이벤트에 의존하므로 hydration 시점 이슈 발생 가능- SSR 환경에서
window를 직접 참조할 수 없음 - React Server Components에서 사용 불가,
useEffect필요
방법 3. CSS env() + viewport-fit=cover
.bottom-nav {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
채택 이유
env() 방식을 선택한 이유는 명확합니다.
1. JavaScript 불필요
CSS만으로 해결되기 때문에 useEffect나 window 참조가 없습니다. SSR / Server Component 환경에서도 문제없이 동작합니다.
2. 기기 대응 자동화
env(safe-area-inset-bottom)은 브라우저가 현재 기기의 safe area 값을 자동으로 주입합니다. 홈 버튼이 있는 기기(iPhone SE 등)는 0, 홈 인디케이터가 있는 기기(iPhone X~)는 약 34px 등 기기에 맞게 자동 처리됩니다.
3. iOS / AOS 모두 지원
iOS Safari는 물론, Android Chrome PWA에서도 동작합니다. Android도 기기마다 제스처 바 영역이 다른데, 이 값도 브라우저가 자동으로 처리해 줍니다.
4. 폴백 지원
env(safe-area-inset-bottom, 0px) — 두 번째 인자로 폴백 값을 지정할 수 있어, env()를 지원하지 않는 구형 브라우저에서도 안전하게 동작합니다.
실제 구현
1. viewport-fit=cover 설정
Next.js 14 App Router에서는 viewport export로 설정합니다.
// app/layout.tsx
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
viewportFit: 'cover', // ← 이게 핵심
}
viewport-fit=cover가 없으면 env(safe-area-inset-bottom)이 항상 0을 반환합니다. 반드시 설정해야 합니다.
2. BottomNav CSS
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
min-height: 56px;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
height: 56px 고정 대신 min-height: 56px로 바꿔야 합니다. 고정 height를 쓰면 padding을 늘려도 컨테이너가 늘어나지 않아 아이콘이 잘립니다.
3. 콘텐츠 하단 여백
바텀 네비가 safe area만큼 늘어났으니, 페이지 콘텐츠도 그만큼 밀어줘야 합니다.
@media (max-width: 480px) {
main {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 2rem);
}
}
4. FAB(플로팅 버튼) 위치 조정
바텀 네비 위에 뜨는 플로팅 버튼도 함께 올려줘야 합니다.
.fab-button {
bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 1.25rem);
}

정리
| 방법 | 장점 | 단점 |
|---|---|---|
| 고정 패딩 | 단순함 | 기기마다 값 다름, 하드코딩 |
| JS 감지 | 정확한 값 측정 가능 | SSR 불가, 코드 복잡, 이벤트 의존 |
| CSS env() | JS 불필요, 자동 대응, SSR 안전 | viewport-fit=cover 설정 필수 |
마치며
PWA를 처음 만들면서 safe area 처리가 이렇게 섬세한 영역인지 몰랐습니다. 브라우저에서는 자동으로 처리되는 것들이 standalone 모드에서는 개발자의 몫이 된다는 점이 흥미로웠습니다.
CSS env() + viewport-fit=cover 조합은 코드도 가장 단순하고, 기기 대응도 자동이며, SSR 환경에서도 완벽하게 동작합니다. PWA를 개발하면서 safe area 문제를 겪고 계신 분들께 도움이 되길 바랍니다.
더 효율적인 방법이나 제가 놓친 부분이 있다면 댓글로 공유해 주세요. 다양한 관점의 피드백을 통해 더 좋은 방향을 찾아가고 싶습니다.
