모바일 사파리 100vh 스크롤 깨짐 해결! dvh 단위 활용법

 

"완벽한 모바일 전체 화면을 만들었는데, 사파리 하단 바가 버튼을 가려버려요!" 프론트엔드 개발자들의 뒷목을 잡게 하는 모바일 브라우저의 100vh 스크롤 깨짐 현상. 이제 복잡한 자바스크립트 꼼수 대신 새로운 CSS 단위인 'dvh'로 깔끔하게 해결하는 방법을 알아봅니다!

 

웹 브라우저 화면에 딱 맞는 풀스크린(Full-screen) UI나 모달 창을 만들 때, 우리는 자연스럽게 CSS에 height: 100vh;를 적어 넣습니다. PC 브라우저나 크롬 개발자 도구의 모바일 화면(Toggle Device Toolbar)으로 테스트할 때까지만 해도 아주 완벽해 보이죠. "오, 스크롤 없이 화면에 꽉 차게 잘 들어갔군!" 하며 뿌듯한 마음으로 스마트폰을 꺼내 테스트 링크를 열어보는 순간... 절망이 시작됩니다.

아이폰 모바일 사파리(Safari)로 접속했더니, 화면 맨 밑에 있어야 할 '확인' 버튼이 사파리의 주소창(하단 바)에 가려져서 보이지 않는 현상을 마주하게 됩니다. 스크롤을 위아래로 낑낑대며 움직여봐도 UI가 덜덜거리고 완전히 깨져버리죠. 진짜 별거 아닌 것 같은데 이것 때문에 완전 짜증 났던 경험, 프론트엔드 개발자라면 누구나 한 번쯤 있으실 거예요. 오늘은 이 지긋지긋한 100vh 버그의 원인과, 최신 CSS가 내려준 한 줄기 빛인 dvh(Dynamic Viewport Height)를 활용한 완벽한 해결법을 낱낱이 파헤쳐 보겠습니다! 😊

 

An isometric 3D digital illustration comparing two mobile phone screens, showcasing responsive web design viewport height issues and their solution. The left screen, labeled 'BAD LAYOUT: Hidden button behind dynamic UI', displays a registration form where the green 'SUBMIT' button is hidden and obstructed behind a Safari-style bottom browser UI address bar, making it unclickable. A large red 'X' covers this error. In the center, a graphic titled 'Viewport Height Comparison' with wireframe phone icons connects to a floating code snippet box containing the specific CSS line: `.container { height: 100dvh; }`, which represents the dynamic viewport height solution. The right screen, labeled 'FIXED LAYOUT: Adaptive button using dynamic DVH', displays the same form, but the UI is adaptive. The green 'SUBMIT' button is now positioned perfectly and accessibly above the Safari browser address bar. A glowing green checkmark (✔) marks this success. The entire scene has a clean, high-tech, digital art style with a teal and light grey color palette and subtle circuit board background patterns.

1. 기존 100vh의 치명적인 문제점 🤔

문제를 해결하려면 도대체 왜 이런 일이 벌어지는지부터 알아야겠죠? vh(Viewport Height)는 말 그대로 보여지는 화면 영역의 높이를 100등분 한 단위입니다. 문제는 모바일 브라우저(특히 iOS 사파리나 안드로이드 크롬)가 '주소창(URL Bar)'과 '네비게이션 바'의 존재를 무시하고 100vh를 계산한다는 데 있습니다.

모바일 사파리는 사용자가 스크롤을 내리면 주소창이 작아지거나 숨겨지고, 스크롤을 올리면 주소창이 다시 나타나는 '동적(Dynamic)'인 구조를 가지고 있습니다. 하지만 기존의 100vh는 주소창이 완전히 축소되었을 때의 가장 긴 화면 상태를 기준으로 높이를 고정해 버립니다. 그래서 주소창이 떡하니 떠 있는 기본 상태에서는 100vh로 잡은 레이아웃의 밑단이 주소창 뒤로 숨어버리는 참사가 발생하는 것이죠.

⚠️ 주의하세요! (JS 꼼수의 한계)
과거에는 이 문제를 해결하기 위해 JavaScript로 window.innerHeight 값을 구한 뒤, CSS 변수(--vh)에 담아 height: calc(var(--vh, 1vh) * 100); 처럼 적용하는 꼼수를 많이 썼습니다. 하지만 이 방식은 창 크기가 변할 때마다 리사이즈 이벤트를 발생시켜 성능 저하(Reflow)를 일으키고, 화면이 번쩍거리는 단점이 있었습니다.

 

2. 새로운 구원자 등장! dvh (Dynamic Viewport Height) ✨

개발자들의 원성이 자자해지자, CSS 표준 위원회에서 마침내 새로운 Viewport 단위들을 발표했습니다. 이제 상황에 맞게 화면 높이를 유연하게 제어할 수 있는 세 가지 새로운 무기가 생겼습니다.

새로운 단위 의미와 작동 방식
svh (Small) 주소창 등 브라우저 UI가 가장 크게 확장되었을 때(가용 화면이 가장 작을 때)를 기준으로 한 100% 높이입니다.
lvh (Large) 주소창이 완전히 축소되거나 숨겨졌을 때(가용 화면이 가장 클 때)를 기준으로 한 100% 높이입니다. (기존 vh와 유사함)
dvh (Dynamic) 사용자의 스크롤에 따라 주소창이 커지고 작아지는 것에 맞춰 높이값이 실시간으로 동적(Dynamic)으로 변하는 100% 높이입니다.

우리가 원했던 바로 그 기능이 dvh입니다! height: 100dvh;를 주면, 처음에 사파리 하단 바가 크게 떠 있을 때는 그 하단 바 바로 위까지만 레이아웃을 그려주고, 스크롤을 해서 하단 바가 사라지면 화면 전체로 레이아웃을 부드럽게 늘려줍니다. 콘텐츠가 UI 뒤로 숨는 일이 완벽하게 사라지는 것이죠.

 

3. 실전 적용 가이드 및 하위 호환성 (Fallback) 💻

적용 방법은 허무할 정도로 간단합니다. 기존에 작성했던 100vh100dvh로 바꿔주기만 하면 됩니다. 하지만 여기서 한 가지 짚고 넘어가야 할 점이 있습니다. 바로 구형 브라우저 대응(하위 호환성)입니다.

dvh 단위는 iOS 15.4 (2022년 3월 출시)부터 지원하기 시작했습니다. 비교적 최신 문법이기 때문에, 업데이트를 안 한 옛날 스마트폰에서는 100dvh라는 단어를 이해하지 못하고 화면 높이를 0으로 만들어 버릴 위험이 있습니다. 따라서 아래와 같이 CSS를 작성하는 것이 가장 안전한 '정석'입니다.

.full-screen-container {
  /* 1. dvh를 모르는 구형 브라우저를 위한 Fallback (기존 vh) */
  height: 100vh;

  /* 2. 최신 브라우저는 아래 코드를 읽어 덮어씌웁니다. */
  height: 100dvh;
}
💡 알아두세요! CSS @supports 활용법
조금 더 엄격하게 코드를 분리하고 싶다면 CSS의 @supports 기능을 사용할 수도 있습니다.
@supports (height: 100dvh) { .container { height: 100dvh; } }
하지만 실무에서는 위 예시처럼 두 줄을 연달아 적는 CSS Cascading 특성을 이용하는 것이 훨씬 간편하고 널리 쓰입니다.

이론만 들으면 심심하니까, 모바일에서 주소창 때문에 발생하는 픽셀 차이를 가상으로 체감해 볼 수 있는 간단한 시뮬레이터를 준비했습니다.

📏 100vh vs 100dvh 가상 높이 시뮬레이터

스마트폰의 전체 화면 길이가 800px이고, 사파리 하단 바 높이가 100px이라고 가정해 봅시다.

핵심 요약 📝

💡

100vh 스크롤 버그 해결 요약 노트

1. 원인 파악: 모바일 브라우저(사파리 등)는 100vh를 계산할 때 하단 주소창이 가리는 영역을 무시하기 때문에 UI가 잘리게 됩니다.
2. 해결사 dvh: Dynamic Viewport Height의 약자로, 브라우저 UI 변화에 맞춰 동적으로 높이가 조절되는 새로운 단위입니다.
3. 실전 코드 적용:
height: 100vh; /* Fallback */
height: 100dvh;
구형 기기를 위해 기존 vh를 먼저 적어주는 센스가 필요합니다.

자주 묻는 질문 ❓

Q: dvh를 사용하면 스크롤할 때마다 레이아웃이 계속 변해서 성능이 떨어지지 않나요?
A: 걱정하지 않으셔도 됩니다! 과거 JS를 이용해 resize 이벤트로 강제 계산하던 방식(1vh 꼼수)은 브라우저 렌더링에 큰 부하를 주었지만, 네이티브 CSS 단위인 dvh는 브라우저 엔진 차원에서 부드럽고 가볍게 최적화되어 동작합니다.
Q: 안드로이드 크롬(Chrome)에서도 동일하게 발생하고 해결되나요?
A: 네, 맞습니다. 모바일 사파리만큼 악명 높지는 않지만 안드로이드 크롬 역시 동적 주소창을 가지고 있어 동일한 이슈가 발생합니다. dvh는 안드로이드 크롬 108 버전 이상에서도 완벽하게 호환되어 이 문제를 해결해 줍니다.
Q: svh와 dvh 중 어떤 것을 써야 할지 헷갈려요.
A: 화면 하단에 고정된 모달이나 네비게이션 바를 만들어서 절대 화면 밖으로 나가지 않게 하려면 svh(가장 좁은 화면 기준)를 쓰는 것이 안전합니다. 반면, 전체 화면을 자연스럽게 덮으면서 스크롤에 반응하는 부드러운 배경이나 일반 레이아웃에는 dvh를 추천합니다.

매번 모바일 뷰만 잡으려고 하면 속을 썩이던 100vh 버그! 이제는 구질구질한 자바스크립트 코드 다 지워버리고, 모던 CSS가 제공하는 dvh 한 줄로 우아하게 프론트엔드 개발의 질을 높여보세요. 혹시 실무에 적용해 보시다가 헷갈리는 점이 있다면 언제든 편하게 댓글로 질문 남겨주세요~ 😊