화면 사이즈가 변화하는 걸 잘 감지하는 방법

브라우저 창 사이즈가 변경되는 걸 더 효율적으로 감지할 수 있는 방법을 기록했습니다.

·

2 min read

Resize 이벤트

인터넷에 검색했을 때 화면 사이즈가 변경되는 걸 감지하는 방법은 resize 이벤트를 감지하여 콜백 함수를 실행하게 하는 것이다.

브라우저 창이 최소 1픽셀씩 변경될 때마다 매번 콜백 함수가 실행되므로 비효율적이며 웹 페이지의 성능에 악영향을 줄 수 있다.

창 크기가 변경되었을 때 변경된 엘리먼트의 크기를 구하려면 getBoundingClientRect 메소드를 실행해야 하는데 호출하게 되면 브라우저는 모든 엘리먼트의 스타일과 레이아웃을 재계산하는 Reflow가 발생한다.

useEffect(() => {
  useEffect(() => {
    const onResize = function (event: UIEvent) {
      console.log("Resizing...");
    };
    window.addEventListener("resize", onResize);
    return () => {
      window.removeEventListener("resize", onResize);
    };
  }, []);
}, []);

Resize Observer

Resize Observer로 특정 엘리먼트가 변화할 때 함수를 호출하도록 할 수 있다.

브라우저 창이 변경하면 document.body 엘리먼트가 변화하는 걸 이용하여 옵저버로 body 엘리먼트를 관찰하게 한다.

각 엔트리마다 contentRect로 엘리먼트의 레이아웃 정보를 가져올 수 있다.

React 프로젝트에서는 useEffect에 옵저버를 선언 후 엘리먼트를 관찰하고 관찰을 해제하는 함수를 반환하도록 한다.

const onResize = useCallback(() => {
  console.log("Resizing...");
}, []);
useEffect(() => {
  const observer = new ResizeObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.target.isSameNode(document.body)) {
        console.log(entry.contentRect);
        onResize();
      }
    });
  });

  observer.observe(document.body);
  return () => {
    observer.disconnect();
  };
}, [onResize]);

useMediaQuery

만약 특정 사이즈 이하일 때만 컴포넌트를 렌더링하거나 특정 함수를 실행시키고 싶으면 Window 객체의 matchMedia 메소드를 적용할 수 있다.

메소드가 반환하는 객체에 이벤트 리스너를 설정해서 창 크기가 변할 때마다 인자로 주어진 조건에 해당하는지 알 수 있다.

React 프론트엔드에서는 window 객체의 상태와 동기화하기 위해 useSyncExternalStore를 사용할 수 있다.

  • SSR 환경에서는 서버에서도 렌더링이 발생하기 때문에 getServerSnapshot 인자도 전달해야 한다.
// ./src/hooks/useMediaQuery.ts
import { useCallback, useSyncExternalStore } from "react";

export interface UseMediaQueryProps {
  query?: string;
}

const getServerSnapshot = () => null;

const useMediaQuery = (props: UseMediaQueryProps) => {
  const { query } = props;

  const onSubscribe = useCallback(
    (onStoreChange: () => unknown) => {
      if (!query) {
        return () => {};
      }
      const matchMedia = window.matchMedia(query);
      matchMedia.addEventListener("change", onStoreChange);
      return () => {
        matchMedia.removeEventListener("change", onStoreChange);
      };
    },
    [query]
  );

  const onSnapshot = useCallback(() => {
    if (!query) {
      return false;
    }
    return window.matchMedia(query).matches;
  }, [query]);

  return useSyncExternalStore(onSubscribe, onSnapshot, getServerSnapshot);
};

export default useMediaQuery;

// ./src/Example.tsx
export default function Example() {
  const isMobileSize = useMediaQuery({
    query: "(max-width: 480px)",
  });
  const isTabletSize = useMediaQuery({
    query: "(min-width: 480px) and (max-width: 720px)",
  });

  if (isMobileSize) {
    return <ExampleMobile />;
  }
  if (isTabletSize) {
    return <ExampleTablet />;
  }
  return <Example />;
}