lifecarelog
프론트엔드

모바일 100vh가 주소창에 잘리는 문제, svh/dvh로 끝낸 기록

모바일 브라우저에서 100vh가 주소창 높이만큼 잘리는 이유와 svh·lvh·dvh 단위로 JS 없이 해결한 과정을 정리했어요.

8분 읽기

풀스크린 히어로 화면을 만들 때 height: 100vh를 쓰는 건 거의 반사적이죠. 그런데 폰에서 열어 보면 아래쪽이 주소창에 가려서 버튼이 반쯤 잘려 보이는 경험, 다들 한 번쯤 하셨을 거예요. 저도 계산기 화면 하나 만들다가 이걸 또 밟아서, 이번엔 제대로 정리해 뒀습니다.

왜 잘리는지부터

모바일 브라우저의 100vh는 주소창이 숨겨진 상태의 큰 뷰포트 높이를 기준으로 잡혀요. 페이지를 처음 열면 주소창이 보이는 상태니까, 화면에 실제로 보이는 영역보다 100vh가 더 큽니다. 아이폰 사파리 기준으로 주소창·툴바가 차지하는 높이가 보통 80~110px 정도라, 딱 그만큼이 화면 밖으로 밀려나요.

더 짜증나는 건 스크롤할 때 주소창이 접히면서 뷰포트 높이가 실시간으로 변한다는 점이에요. 고정값이 아니라 움직이는 값이니, 이걸 픽셀로 잡으려는 시도 자체가 무리였던 거죠.

페이지 진입(주소창 보임)100vh가 화면보다 큼하단 잘림스크롤로 주소창 접힘높이 또 변함

예전에 쓰던 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)으로 쓰는 방식이죠. 동작은 합니다. 다만 직접 붙여 보니 걸리는 게 세 가지 있었어요.

  1. 첫 페인트 전에 값이 없어서 순간적으로 레이아웃이 튀어요
  2. 스크롤 중 resize가 계속 발생해서 리플로우가 반복돼요
  3. 컴포넌트마다 이 훅을 챙겨야 해서 잊어버리기 쉬워요

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 변수를 세팅하는 코드를 발견하면, 한 번쯤 걷어낼 수 있는지 확인해 보세요. 대부분은 두 줄이면 충분하더라고요.

#CSS#모바일웹#뷰포트

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.