개발 기록 남기기✍️

모바일 환경에서 키패드 등장에 따른 반응형 구현하기 본문

개발 일기장

모바일 환경에서 키패드 등장에 따른 반응형 구현하기

너해동물원 2024. 3. 3. 17:46

💥 Issue

마주친 상황은 다음과 같습니다.

☝🏻 다음 버튼은 기본적으로 화면 하단에 위치하고, 키패드가 등장하면 버튼은 줄어든 화면만큼 키패드와 함께 올라가야 해요

 

뭐야~ 레이아웃 컴포넌트의 height를 100vh로 설정하면 끝나는거 아님?

 

아님

 

 

브라우저 뷰포트 높이

 

모바일 환경에서는 높이를 100vh로 설정하더라도 동적 툴바(주소, 네비게이션바)로 인해 스크롤이 발생하기 때문이죠!

 

 

해당 현상은 다음 속성을 통해 해결할 수 있습니다.

.fill-screen {
  width:100vw;
  height: 100vh; /* 새 단위를 지원하지 않는 소수의 브라우저를 위해 */
  height:100dvh;
}
  • dvh(Dynamic Viewport Height) : svh/lvh 사이에서 탭의 유무에 따라 동적으로 변화
  • svh(Small Viewport Height) : 사용자가 볼 수 있는 가장 작은 viewport 높이. viewport 높이에서 모든 인터페이스 요소를 제거한 높이
  • lvh(Large Viewport Height) : 사용자가 볼 수 있는 가장 큰 viewport 높이. viewport 높이에서 모든 인터페이스 요소를 포함한 높이

 

오 그럼 이제 다 해결된거 아님?

문제를 해결하기 위해 작성했던 코드는 다음과 같아요.

export default function Home() {
  return (
    <main className="flex fill-screen p-5 flex-col items-center justify-between">
      <input
        className="w-full border-slate-500 border-[1px] p-2"
        type="number"
        placeholder="내용을 입력해주세요"
      />
      <button className="w-full p-4 bg-yellow-400 rounded-xl" type="button">
        다음
      </button>
    </main>
  );
}

 

 

 

잘 동작했을까요?

 

 

키패드에 가려지는 나의 작고 소중한 다음 버튼

 

 

 

엉엉

 

 

어림도 없었습니다.

아니 그럼 이 문제를 어떻게 해결할 수 있단 말임?

 

문제를 해결하기 위해서는 모바일 브라우저에서의 viewport에 대해 짚고 넘어가야 해요.

 

🔎 모바일 브라우저의 document viewport

모바일 브라우저에는 Layout viewportVisual viewport 2개의 뷰포트가 존재합니다.

 

  • Layout viewport : 스크롤로 인해 화면 밖으로 표시되는 콘텐츠를 포함한 웹 페이지의 전체 레이아웃
  • Visual viewport : 사용자 디바이스 내에서 웹 페이지의 현재 표시되는 부분

 

가상 키보드(On-Screen Keyboard), 줌인 / 줌아웃과 같은 이벤트는 layout viewport에 영향을 주지 않고 visual viewport의 변경을 일으킵니다.

이 때문에 레이아웃 컴포넌트의 height를 동적으로 설정해도 키패드 등장 시 height가 줄어들지 않았던 것이었어요!

따라서 Visual Viewport Web API를 사용하여 visual viewport에 resize 이벤트가 발생했을 때 visual viewport의 height를 가져와야 합니다.

 


🧐 window.innerHeight로는 안되나요?

Android, iOS Chrome에서는 키보드가 열리면 window.visualViewport.height와 window.innerHeight가 같이 줄어들지만, iOS Safari 브라우저에서는 visualViewport값이 독립적으로 변경되기 때문에 visualViewport에 대한 이벤트 리스너를 추가해주어야 합니다.

 

 

사파리….. 이 자식….. 마음에 안들어…..!!!!!

 

 

🌿 Solve

"use client";
import React from "react";

export default function Home() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  React.useEffect(() => {
    const handleResize = () => {
      if (!containerRef.current) return;

      const height = window.visualViewport
        ? window.visualViewport.height
        : window.innerHeight;
      containerRef.current.style.height = `${height}px`;
      requestAnimationFrame(handleResize);
    };

    if (window.visualViewport) {
      window.visualViewport.addEventListener("resize", handleResize);
    }

    return () => {
      if (window.visualViewport) {
        window.visualViewport.removeEventListener("resize", handleResize);
      }
    };
  }, []);

  return (
    <main
      ref={containerRef}
      className="flex fill-screen p-5 flex-col items-center justify-between"
    >
      <input
        className="w-full border-slate-500 border-[1px] p-2"
        type="number"
        placeholder="내용을 입력해주세요"
      />
      <button className="w-full p-4 bg-yellow-400 rounded-xl" type="button">
        다음
      </button>
    </main>
  );
}
iOS Safari iOS Chrome Android Chrome



사파리에서 프레임 뚝뚝 끊기는게 마음에 안드네요 증맬…. 🫤

웹에서 앱 기능을 구현하려니 계속 한계에 부딪히게 되는데, 진짜 앱 배워야 하나... 고민만 남은 트러블 슈팅이였습니다.🫠