본문으로 건너뛰기
wooncloud

모바일 키보드가 모달을 가릴 때 — visualViewport + paddingBottom 한 방

모바일 웹에서 키보드에 가려지는 모달 문제를 visualViewport API와 paddingBottom을 활용해 해결하는 방법을 소개합니다. 레이아웃 정렬 전략을 바꾸지 않고도 키보드 높이만큼 오버레이 영역을 조정하여 모달이 항상 키보드 위로 올라오게 만드는 실무적인 최적화 노하우를 확인해 보세요

·6 min read· views·

Minimal flat-design illustration split into two ph

모바일 웹/웹뷰에서 입력 모달(이름 변경, 새 폴더 같은 작은 프롬프트)을 띄웠을 때, 입력창에 포커스가 가서 소프트 키보드가 올라오면 모달의 입력창·확인 버튼이 키보드 뒤로 깔려 안 보이는 문제가 있다. 데스크톱 크롬 DevTools 디바이스 모드로는 재현조차 안 돼서 한참 헤매기 쉽다. 정렬 전략을 바꾸지 않고 padding-bottom 하나로 깔끔하게 해결하는 방법을 정리한다.

왜 가려지나

핵심은 iOS Safari / WKWebView 는 키보드가 떠도 레이아웃 뷰포트를 줄이지 않는다는 점이다. 줄어드는 건 "시각 뷰포트(visual viewport)"뿐이다.

그래서 모달 오버레이를 흔히 쓰는 방식으로 잡으면 문제가 생긴다.

.overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

오버레이는 키보드를 무시한 "전체 화면" 기준으로 그려지고, 가운데 정렬된 모달은 화면 정중앙에 위치한다. 키보드가 하단 약 40% 를 덮으면 그 뒤로 묻혀버린다.

게다가 데스크톱 크롬 DevTools 의 디바이스 모드는 OS 소프트 키보드를 띄우지 않는다. 그래서 visualViewport 가 줄어들지 않고, 이 버그는 실기기(또는 원격 디버깅) 에서만 재현된다. 이걸 모르면 "코드는 맞는데 왜 재현이 안 되지" 하며 시간을 버린다.

해결: 가려진 높이만큼 padding-bottom

window.visualViewport키보드가 가린 하단 높이(inset) 를 구해서, 오버레이의 padding-bottom 에 그만큼 더해준다. 그게 전부다.

  • align-items: center 모달 → 사용 가능 영역이 위로 좁아지면서 모달이 키보드 위로 올라온다
  • align-items: stretch 풀스크린 모달 → 패널이 키보드 위 영역으로 줄어든다

즉 정렬 전략을 바꾸지 않고 padding-bottom 하나로 두 케이스를 동시에 해결한다.

inset 계산식은 다음과 같다.

inset = 레이아웃 뷰포트 높이 - (시각 뷰포트 높이 + 위로 밀린 양)
      = window.innerHeight - (visualViewport.height + visualViewport.offsetTop)

코드

키보드가 가린 높이를 추적하는 훅. 미지원 환경·데스크톱은 항상 0 을 반환한다.

import { useEffect, useState } from "react";
 
// 키보드가 가린 화면 하단 높이(px). 미지원/데스크톱은 0.
export function useKeyboardInset() {
  const [inset, setInset] = useState(0);
 
  useEffect(() => {
    const vv = window.visualViewport;
    if (!vv) return;
 
    const update = () => {
      const next = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
      // 주소창 show/hide 같은 미세 변동은 키보드로 오인하지 않게 임계값으로 거른다.
      setInset(next > 80 ? Math.round(next) : 0);
    };
 
    update();
    vv.addEventListener("resize", update);
    vv.addEventListener("scroll", update);
    return () => {
      vv.removeEventListener("resize", update);
      vv.removeEventListener("scroll", update);
    };
  }, []);
 
  return inset;
}

이걸 오버레이의 padding-bottom 에 더한다.

function Modal({ children }) {
  const kb = useKeyboardInset();
 
  return (
    <div
      style={{
        position: "fixed",
        inset: 0,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        padding: 16,
        paddingBottom: 16 + kb, // ← 키보드만큼 더 → 모달이 위로
        transition: "padding-bottom 0.2s ease",
      }}
    >
      <div style={{ maxHeight: "100%", overflowY: "auto" }}>{children}</div>
    </div>
  );
}

패널에 maxHeight: 100%overflowY: auto 를 같이 주는 게 포인트다. 키보드로 영역이 줄어 콘텐츠가 넘쳐도 내부 스크롤로 입력창·버튼에 접근할 수 있다.

임계값으로 노이즈 거르기

모바일 주소창의 show/hide 도 시각 뷰포트를 흔든다. 그래서 80px 같은 임계값 아래는 0 으로 처리해 키보드만 깔끔하게 잡는다.

엄밀히 말하면 주소창 변동은 보통 innerHeight 도 함께 변해서 둘의 차이에는 잘 안 잡힌다. 하지만 기기·브라우저마다 타이밍이 달라 미세 오차가 남을 수 있으니, 임계값은 안전망으로 두는 편이 낫다. 키보드는 보통 250px 이상이라 80px 컷에 걸릴 일이 없다.

참고

  • 안드로이드 크롬 계열에서는 뷰포트 메타의 interactive-widget=resizes-content 로 레이아웃 자체를 키보드에 맞춰 줄일 수도 있다. 다만 iOS 는 미지원이라, 크로스 플랫폼으로는 visualViewport 방식이 더 안정적이다.
  • MDN — VisualViewport
  • MDN — Viewport meta tag