모바일 100vh가 주소창에 잘리는 문제, svh/dvh로 끝낸 기록
모바일 브라우저에서 100vh가 주소창 높이만큼 잘리는 이유와 svh·lvh·dvh 단위로 JS 없이 해결한 과정을 정리했어요.
풀스크린 히어로 화면을 만들 때 height: 100vh를 쓰는 건 거의 반사적이죠. 그런데 폰에서 열어 보면 아래쪽이 주소창에 가려서 버튼이 반쯤 잘려 보이는 경험, 다들 한 번쯤 하셨을 거예요. 저도 계산기 화면 하나 만들다가 이걸 또 밟아서, 이번엔 제대로 정리해 뒀습니다.
왜 잘리는지부터
모바일 브라우저의 100vh는 주소창이 숨겨진 상태의 큰 뷰포트 높이를 기준으로 잡혀요. 페이지를 처음 열면 주소창이 보이는 상태니까, 화면에 실제로 보이는 영역보다 100vh가 더 큽니다. 아이폰 사파리 기준으로 주소창·툴바가 차지하는 높이가 보통 80~110px 정도라, 딱 그만큼이 화면 밖으로 밀려나요.
더 짜증나는 건 스크롤할 때 주소창이 접히면서 뷰포트 높이가 실시간으로 변한다는 점이에요. 고정값이 아니라 움직이는 값이니, 이걸 픽셀로 잡으려는 시도 자체가 무리였던 거죠.
예전에 쓰던 JS 방식
한동안은 이렇게 했어요.
const setVh = () => {
document.documentElement.style.setProperty(
"--vh",
`${window.innerHeight * 0.01}px`
);
};
setVh();
window.addEventListener("resize", setVh);그리고 CSS에서 height: calc(var(--vh, 1vh) * 100)으로 쓰는 방식이죠. 동작은 합니다. 다만 직접 붙여 보니 걸리는 게 세 가지 있었어요.
- 첫 페인트 전에 값이 없어서 순간적으로 레이아웃이 튀어요
- 스크롤 중
resize가 계속 발생해서 리플로우가 반복돼요 - 컴포넌트마다 이 훅을 챙겨야 해서 잊어버리기 쉬워요
React라면 useEffect 하나가 더 붙는 셈인데, 이 정도 문제에 상태 관리를 끌어오는 건 과하다는 생각이 계속 들었습니다.
지금은 CSS 단위로 끝나요
CSS Values and Units Level 4에 들어온 뷰포트 단위가 이 문제를 그대로 겨냥합니다. 세 가지만 기억하면 돼요.
svh·lvh·dvh 세 단위가 주소창 상태에 따라 각각 어떤 높이를 기준으로 삼는지 비교
svh(small viewport height): 주소창이 보이는 상태 기준, 가장 작은 높이lvh(large viewport height): 주소창이 숨겨진 상태 기준, 기존100vh와 같은 값dvh(dynamic viewport height): 지금 이 순간의 실제 높이, 주소창 접힘에 따라 자동으로 따라옴
그래서 실제 코드는 이렇게 짧아집니다.
.hero {
min-height: 100svh;
}진입하자마자 아무것도 잘리지 않아야 하는 화면이라면 svh가 정답이에요. 주소창이 접혔을 때 여백이 조금 생기지만, 버튼이 잘리는 것보다는 훨씬 낫습니다.
어떤 걸 언제 쓰나
직접 여러 화면에 적용해 보면서 정리한 기준이에요.
svh를 쓰는 곳 — 로그인 화면, 온보딩, 랜딩 히어로처럼 첫 화면에 CTA 버튼이 있는 경우. 진입 시점에 다 보여야 하니까요.
dvh를 쓰는 곳 — 바텀시트나 전체화면 모달처럼 스크롤 없이 화면을 꽉 채워야 하는 컴포넌트. 주소창이 움직여도 자연스럽게 따라붙어요.
lvh를 쓰는 곳 — 사실상 거의 없었어요. 배경 이미지처럼 조금 넘쳐도 상관없는 레이어 정도.
한 가지 주의할 점은 dvh를 남발하면 스크롤할 때마다 높이가 변해서 레이아웃이 미묘하게 흔들린다는 거예요. 고정되어야 하는 컨테이너에 쓰면 오히려 어지럽습니다. 기본은 svh, 꼭 필요한 곳만 dvh로 가는 게 편했어요.
구형 브라우저 대비
지원하지 않는 환경을 위해서는 fallback을 한 줄 더 쓰면 됩니다.
.hero {
min-height: 100vh;
min-height: 100svh;
}CSS는 해석하지 못하는 선언을 무시하니까, 새 단위를 아는 브라우저만 두 번째 줄을 적용해요. 별도 분기 없이 이 두 줄로 끝납니다. @supports까지 쓸 필요도 없었어요.
안전영역도 같이
아이폰 하단 홈 인디케이터 영역까지 고려한다면 env()를 같이 쓰면 깔끔해요.
.bottom-bar {
padding-bottom: env(safe-area-inset-bottom, 0px);
}viewport-fit=cover를 메타 태그에 넣어야 env() 값이 실제로 들어옵니다. 이 부분을 빼먹으면 값이 계속 0으로 나와서 한참 헤맬 수 있어요.
정리
- 기본은 100svh
- 동적 컴포넌트만 100dvh
- fallback으로 100vh 한 줄 추가
- env()는 viewport-fit=cover 필수
JS 훅 하나와 리사이즈 리스너를 걷어내고 CSS 두 줄로 바꾸는 데 10분 정도 걸렸어요. 코드 줄 수만 줄어든 게 아니라 초기 렌더링 시 레이아웃이 튀던 것도 같이 사라져서, 체감으로도 훨씬 깔끔해졌습니다.
플랜씨(plan-c) 금융계산기도 로그인 없이 바로 쓰는 구조라 첫 화면 높이가 특히 중요했는데, 계산 결과 영역이 주소창에 가리지 않게 잡는 데 이 방식이 잘 맞았어요. 모바일에서 열어 보면 진입 직후 입력창부터 결과까지 한 화면에 들어옵니다.
혹시 지금 프로젝트에서 --vh 변수를 세팅하는 코드를 발견하면, 한 번쯤 걷어낼 수 있는지 확인해 보세요. 대부분은 두 줄이면 충분하더라고요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.