Front-end#FE

iPhone PWA에서 바텀 네비게이션 바, 홈 인디케이터 영역 안 밟는 법

PWA로 설치된 아이폰에서 바텀 네비게이션 바가 홈 인디케이터를 침범하는 문제를 CSS env()와 viewport-fit=cover로 해결한 방법 정리

2026년 6월 6일3분 읽기조회 15
공유

들어가며

이 블로그(yerim.dev)는 PWA로 설치해서 아이폰에서도 사용할 수 있도록 만들었습니다. 그런데 아이폰에서 열어보니, 하단 고정 네비게이션 바가 홈 인디케이터(제스처 바) 영역에 딱 붙어 있는 문제가 생겼습니다.

일반 Safari 브라우저에서는 브라우저 자체 UI가 safe area를 처리해주지만, PWA(standalone) 모드에서는 개발자가 직접 처리해야 한다는 걸 이때 처음 알게 됐습니다.

이 글에서는 문제를 어떻게 발견했는지, 어떤 방법들을 시도했는지, 그리고 최종적으로 어떤 방법을 선택했는지 그 과정을 정리했습니다.

개선할 점이나 다른 접근 방식이 있다면 댓글로 공유해 주세요! 🙌


문제 상황

iPhone X 이후 기기들은 화면 하단에 홈 인디케이터(얇은 가로 바)가 있습니다. 이 영역은 약 34px로, 이 위에 버튼이나 네비게이션이 올라오면 사용자가 실수로 터치하거나 시각적으로 가려지는 문제가 발생합니다.

PWA standalone 모드에서는 브라우저 UI가 없기 때문에, 이 영역을 개발자가 명시적으로 피해줘야 합니다.

image.png


시도한 방법들

방법 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);
}

image.png

정리

방법장점단점
고정 패딩단순함기기마다 값 다름, 하드코딩
JS 감지정확한 값 측정 가능SSR 불가, 코드 복잡, 이벤트 의존
CSS env()JS 불필요, 자동 대응, SSR 안전viewport-fit=cover 설정 필수

마치며

PWA를 처음 만들면서 safe area 처리가 이렇게 섬세한 영역인지 몰랐습니다. 브라우저에서는 자동으로 처리되는 것들이 standalone 모드에서는 개발자의 몫이 된다는 점이 흥미로웠습니다.

CSS env() + viewport-fit=cover 조합은 코드도 가장 단순하고, 기기 대응도 자동이며, SSR 환경에서도 완벽하게 동작합니다. PWA를 개발하면서 safe area 문제를 겪고 계신 분들께 도움이 되길 바랍니다.

더 효율적인 방법이나 제가 놓친 부분이 있다면 댓글로 공유해 주세요. 다양한 관점의 피드백을 통해 더 좋은 방향을 찾아가고 싶습니다.

관련 포스트

댓글

방명록을 남겨주세요