모바일 사파리 100vh 스크롤 깨짐 해결! dvh 단위 활용법
웹 브라우저 화면에 딱 맞는 풀스크린(Full-screen) UI나 모달 창을 만들 때, 우리는 자연스럽게 CSS에 height: 100vh;를 적어 넣습니다. PC 브라우저나 크롬 개발자 도구의 모바일 화면(Toggle Device Toolbar)으로 테스트할 때까지만 해도 아주 완벽해 보이죠. "오, 스크롤 없이 화면에 꽉 차게 잘 들어갔군!" 하며 뿌듯한 마음으로 스마트폰을 꺼내 테스트 링크를 열어보는 순간... 절망이 시작됩니다.
아이폰 모바일 사파리(Safari)로 접속했더니, 화면 맨 밑에 있어야 할 '확인' 버튼이 사파리의 주소창(하단 바)에 가려져서 보이지 않는 현상을 마주하게 됩니다. 스크롤을 위아래로 낑낑대며 움직여봐도 UI가 덜덜거리고 완전히 깨져버리죠. 진짜 별거 아닌 것 같은데 이것 때문에 완전 짜증 났던 경험, 프론트엔드 개발자라면 누구나 한 번쯤 있으실 거예요. 오늘은 이 지긋지긋한 100vh 버그의 원인과, 최신 CSS가 내려준 한 줄기 빛인 dvh(Dynamic Viewport Height)를 활용한 완벽한 해결법을 낱낱이 파헤쳐 보겠습니다! 😊
1. 기존 100vh의 치명적인 문제점 🤔
문제를 해결하려면 도대체 왜 이런 일이 벌어지는지부터 알아야겠죠? vh(Viewport Height)는 말 그대로 보여지는 화면 영역의 높이를 100등분 한 단위입니다. 문제는 모바일 브라우저(특히 iOS 사파리나 안드로이드 크롬)가 '주소창(URL Bar)'과 '네비게이션 바'의 존재를 무시하고 100vh를 계산한다는 데 있습니다.
모바일 사파리는 사용자가 스크롤을 내리면 주소창이 작아지거나 숨겨지고, 스크롤을 올리면 주소창이 다시 나타나는 '동적(Dynamic)'인 구조를 가지고 있습니다. 하지만 기존의 100vh는 주소창이 완전히 축소되었을 때의 가장 긴 화면 상태를 기준으로 높이를 고정해 버립니다. 그래서 주소창이 떡하니 떠 있는 기본 상태에서는 100vh로 잡은 레이아웃의 밑단이 주소창 뒤로 숨어버리는 참사가 발생하는 것이죠.
과거에는 이 문제를 해결하기 위해 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) 💻
적용 방법은 허무할 정도로 간단합니다. 기존에 작성했던 100vh를 100dvh로 바꿔주기만 하면 됩니다. 하지만 여기서 한 가지 짚고 넘어가야 할 점이 있습니다. 바로 구형 브라우저 대응(하위 호환성)입니다.
dvh 단위는 iOS 15.4 (2022년 3월 출시)부터 지원하기 시작했습니다. 비교적 최신 문법이기 때문에, 업데이트를 안 한 옛날 스마트폰에서는 100dvh라는 단어를 이해하지 못하고 화면 높이를 0으로 만들어 버릴 위험이 있습니다. 따라서 아래와 같이 CSS를 작성하는 것이 가장 안전한 '정석'입니다.
/* 1. dvh를 모르는 구형 브라우저를 위한 Fallback (기존 vh) */
height: 100vh;
/* 2. 최신 브라우저는 아래 코드를 읽어 덮어씌웁니다. */
height: 100dvh;
}
조금 더 엄격하게 코드를 분리하고 싶다면 CSS의
@supports 기능을 사용할 수도 있습니다.@supports (height: 100dvh) { .container { height: 100dvh; } }하지만 실무에서는 위 예시처럼 두 줄을 연달아 적는 CSS Cascading 특성을 이용하는 것이 훨씬 간편하고 널리 쓰입니다.
이론만 들으면 심심하니까, 모바일에서 주소창 때문에 발생하는 픽셀 차이를 가상으로 체감해 볼 수 있는 간단한 시뮬레이터를 준비했습니다.
📏 100vh vs 100dvh 가상 높이 시뮬레이터
스마트폰의 전체 화면 길이가 800px이고, 사파리 하단 바 높이가 100px이라고 가정해 봅시다.
핵심 요약 📝
100vh 스크롤 버그 해결 요약 노트
height: 100dvh;
자주 묻는 질문 ❓
svh(가장 좁은 화면 기준)를 쓰는 것이 안전합니다. 반면, 전체 화면을 자연스럽게 덮으면서 스크롤에 반응하는 부드러운 배경이나 일반 레이아웃에는 dvh를 추천합니다.매번 모바일 뷰만 잡으려고 하면 속을 썩이던 100vh 버그! 이제는 구질구질한 자바스크립트 코드 다 지워버리고, 모던 CSS가 제공하는 dvh 한 줄로 우아하게 프론트엔드 개발의 질을 높여보세요. 혹시 실무에 적용해 보시다가 헷갈리는 점이 있다면 언제든 편하게 댓글로 질문 남겨주세요~ 😊