05 — Viewport Units (dvh·svh·lvh)
답하는 질문: 왜
100vh가 iOS Safari에서 잘렸는가? 그리고dvh는 무엇이 다른가?
한 줄 답
vh는 최대 뷰포트(브라우저 chrome 숨김 가정)를 가리키므로, iOS Safari처럼 주소창이 동적으로 나타나고 사라지는 환경에서 잘림이 발생한다. CSS Values Level 4(2022)는 이를svh/lvh/dvh/dvw4종 단위로 해결 — 작은 뷰포트, 큰 뷰포트, 동적 뷰포트. iOS 사용자가 5년간 보고 있던 검은 띠가 사라졌다.
Why — 왜 vh로는 안 됐나
1) iOS Safari의 동적 chrome
iOS Safari는 사용자가 스크롤하면 주소창을 숨긴다. 즉 viewport의 높이가 두 가지다:
- chrome 표시 시: 약 670px (iPhone 13 기준)
- chrome 숨김 시: 약 740px
2) vh의 정의 — 최대 가정
CSS Values Level 3(2012)은 vh를 최대 뷰포트 높이로 정의. iOS는 chrome 숨김 시점의 740px을 기준으로 100vh = 740px로 고정 평가.
결과:
- chrome 표시 중 → 실제 보이는 영역 670px인데
100vh = 740px→ 70px 아래로 잘림. - 풀스크린 영웅 이미지에 검은 띠 출현, “Continue” 버튼이 주소창 아래에 가려짐.
3) 5년의 우회
/* JS로 매번 측정 */
:root { --vh: 1vh; }
window.addEventListener('resize', () => {
document.documentElement.style.setProperty('--vh', `${window.innerHeight / 100}px`);
});
.hero { height: calc(var(--vh) * 100); }iOS의 주소창 토글 이벤트는 표준 resize가 아니다 — visualViewport API까지 동원해야 했다. 이 패턴은 2017~2022년 모든 모바일 사이트에 존재했다.
How — 4종 단위의 동작
4종 단위 (CSS Values Level 4, 2022)
| 단위 | 의미 | 사용처 |
|---|---|---|
lvh (Large) | Large viewport — chrome 숨김 시 최대 높이 | 거의 안 씀 |
svh (Small) | Small viewport — chrome 표시 시 최소 높이 | 안전한 fallback |
dvh (Dynamic) | Dynamic — 현재 시점 실제 높이 | 대부분의 경우 |
vh (legacy) | lvh와 같음 — 잘림 가능 | 비권장 |
폭에도 동일 — dvw/svw/lvw/vw. 가로 chrome(태블릿 노치)이 있는 환경에서 의미 있음.
다른 축 — dvi/dvb(inline/block), dvmin/dvmax도 있음.
What — 구체 사용 패턴
1) 풀스크린 히어로 — dvh 권장
.hero {
min-height: 100dvh; /* 항상 현재 viewport에 맞춤 */
}→ iOS에서 chrome 토글 시 hero가 자연스럽게 늘었다 줄었다 함.
2) 잘림 방지가 우선이면 svh
.hero {
min-height: 100svh; /* 보수적 — 가장 작은 viewport에 맞춤 */
}→ chrome 표시 중에도 절대 잘리지 않음. 단점: chrome 숨김 시 아래에 빈 공간 약 70px.
3) 안전한 폴백 패턴
.hero {
min-height: 100vh; /* 구형 브라우저 폴백 */
min-height: 100dvh; /* 모던 브라우저 */
}CSS는 아래 선언이 파싱 가능하면 위를 덮어씀 — dvh를 모르는 브라우저는 무시하고 vh 사용.
4) 안전영역(env)과 조합
.hero {
min-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}노치, 홈 인디케이터 안전 영역까지 고려.
5) 컴포넌트 단위 cqh와의 구분
vh/dvh— 뷰포트 기준.cqh— 컨테이너 기준 (02-container-queries).
둘은 경쟁이 아니라 보완 — 페이지 전체는 dvh, 카드 내부는 cqi/cqh.
What-if — 잘못 쓰면
Case 1: 100dvh만 쓰고 폴백 없음
.hero { height: 100dvh; } /* 구형 Android, 회사 인앱 브라우저 무시 */dvh 미지원 브라우저에서 높이 0 또는 기본값 auto로 해석. 폴백 필수:
.hero {
height: 100vh;
height: 100dvh;
}Case 2: 키보드 등장 시 레이아웃 점프
iOS에서 input focus → 가상 키보드 등장 → dvh가 키보드 위 영역으로 축소 → 페이지 레이아웃 점프.
해결:
- 키보드가 뜨는 화면에는
svh사용(보수적). - 또는
visualViewport.height를 JS로 구독해 키보드 등장 중에는 dvh 변경 무시.
Case 3: dvh의 덜컹거림
iOS Safari에서 스크롤로 chrome이 드래그되어 사라지는 중에는 dvh가 프레임마다 다른 값 — 100dvh로 잡힌 영웅이 반응적으로 늘어남.
이 연속 변화가 어색하면:
svh로 고정하는 게 시각적으로 안정.- 또는 transition으로 부드럽게 —
transition: min-height 0.2s.
Case 4: 100lvh로 가득 채웠더니 chrome이 안 숨겨짐
iOS는 스크롤 가능한 콘텐츠가 있어야 chrome을 숨긴다. 100lvh로 정확히 뷰포트만 채우면 스크롤이 없어 chrome이 영원히 표시. → 의도와 다르면 콘텐츠가 살짝 넘치도록 설계.
Case 5: tabletop / 데스크탑 모니터 회전
회전 시 dvw/dvh가 교환됨 — 100dvh가 갑자기 1280px(가로 모드)이 될 수 있음. aspect-ratio 기반 미디어 쿼리와 결합 검토.
Insight — “vh의 거짓말”이 만든 5년
2017년경부터 “100vh가 iOS에서 안 맞는 문제” 는 Stack Overflow의 단골 질문이었다. 매년 새로운 우회법이 등장했다 — --vh 변수, visualViewport API, webkit-fill-available…
CSSWG가 2020년부터 Values Level 4 Editor’s Draft에 svh/lvh/dvh를 넣었지만, 합류는 2022년 Chrome 108, Safari 15.4. 5년의 누적된 우회 코드가 한 줄로 줄어든 사건.
이 사례는 표준의 의도와 현실의 괴리를 보여준다. vh의 최대 가정은 2012년 데스크탑 사고방식 — 그러나 iOS Safari가 등장하면서 동적 viewport가 현실이 됐다. 표준이 현실을 따라잡는데 10년이 걸렸다.
반응형의 정의는 디바이스에서 상태로 확장됐다. chrome이 있다 vs 없다는 디바이스가 아니라 상태 — 이를 CSS로 표현하려면
vh로는 부족했다.
Bramus(Chrome DevRel)는 이를 “the most consequential CSS unit since rem” 이라 평했다.
요약 + Mermaid
vh= 최대 viewport → iOS chrome 표시 중 잘림.- 4종 단위로 분리 —
lvh(최대),svh(최소),dvh(현재),vh(legacy=lvh). - 대부분
dvh권장, 키보드 화면은svh. - 폴백 —
vh먼저,dvh뒤. env(safe-area-inset-*)와 조합으로 노치 대응.
다음: 06-fluid-design — 미디어 쿼리 없이 적응하는 디자인.