🎨 Frontend CSS7. 반응형 (Media·Container)05 — Viewport Units (dvh·svh·lvh)

05 — Viewport Units (dvh·svh·lvh)

답하는 질문: 왜 100vh가 iOS Safari에서 잘렸는가? 그리고 dvh는 무엇이 다른가?


한 줄 답

vh최대 뷰포트(브라우저 chrome 숨김 가정)를 가리키므로, iOS Safari처럼 주소창이 동적으로 나타나고 사라지는 환경에서 잘림이 발생한다. CSS Values Level 4(2022)는 이를 svh/lvh/dvh/dvw 4종 단위로 해결 — 작은 뷰포트, 큰 뷰포트, 동적 뷰포트. 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 = 740px70px 아래로 잘림.
  • 풀스크린 영웅 이미지에 검은 띠 출현, “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 Draftsvh/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미디어 쿼리 없이 적응하는 디자인.