본문 바로가기
Front-End

[NextJS] Viewport에 들어온 컴포넌트만 불러오기

by kimik 2022. 4. 17.

NextJS에는 코드스플리팅을 위한 Dynamic import라는 기능이 있습니다. 

 

어떤 컴포넌트를 특정 조건에서만 렌더링할때(모달 혹은 hover/ click시 노출되는 컴포넌트가 대표적)

 

그 컴포넌트를 위한JS를 해당 조건이 true일때 import해오는 기능입니다. 이 기능을 활용하지 않고,

 

NextJS로 페이지를 구성하면 lighthouse의 퍼포먼스에서 처참한 점수를 보실 수도 있습니다. 

 

아무튼 이 기능은 사용하기 어렵지 않지만, 섹션 하나하나가 모여 거대한 한페이지를 이루는

 

랜딩/이벤트 등의 페이지에서 단순하게 사용하면 퍼포먼스적인 이득을 보기가 힘듭니다.

 

그 이유는 viewport에 보이지 않는 컴포넌트도 전부 렌더링하기 때문인데,

 

이를 처리하기 위해서는 해당 컴포넌트가 viewport에 보였는지를 체크하여

 

dynamic import하는 형태로 만들어줘야합니다.

 

그럼 특정 컴포넌트가 viewport에 들어오는걸 감지하는 hook과 dynamic import를

 

이용하여 간단하게 만들어 보겠습니다.

 

1. NextJS 설치 (이미 NextJS를 사용중이라면 skip)

 

npx create-next-app dynamic-import-components --typescript

 

2. hook 만들기

import { RefObject, useEffect, useState } from "react";

interface Args extends IntersectionObserverInit {
  freezeOnceVisible?: boolean; //true 일때 isIntersecting가 바뀌면 observer중지
  prefetch?: boolean; //observer를 활용하지않고 바로 import
}

function useIntersectionObserver(
  elementRef: RefObject<Element>,
  {
    threshold = 0,
    root = null,
    rootMargin = "0%",
    freezeOnceVisible = true,
    prefetch = false,
  }: Args
): IntersectionObserverEntry | undefined {
  const [entry, setEntry] = useState<IntersectionObserverEntry>();

  const frozen = entry?.isIntersecting && freezeOnceVisible;

  const updateEntry = (
    [entry]: IntersectionObserverEntry[],
    observer: IntersectionObserver
  ): void => {
    setEntry(entry);
  };

  useEffect(() => {
    const node = elementRef?.current;
    const hasIOSupport = !!window.IntersectionObserver;

    if (!hasIOSupport || frozen || !node) return;

    const observerParams = { threshold, root, rootMargin };
    const observer = !prefetch
      ? new IntersectionObserver(updateEntry, observerParams)
      : undefined;
    observer?.observe(node);

    return () => observer?.disconnect();
  }, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen]);

  return entry;
}

export default useIntersectionObserver;

(IntersectionObserver에 대해 설명하는 포스트가 아니기때문에 자세한 설명은 여기를 참고해주세요! 

그리고 저는 약간의 수정을 했을뿐이므로 원본은 여기!)

 

컴포넌트에서 활용할 hook입니다. 옵션은 2가지로 IntersectionObserver를 활용하지않고,

 

그냥 바로 import해오는 prefetch

 

해당 컴포넌트에 대한 isIntersecting이 변경되면 더 이상 Observer하지 않는

 

freezeOnceVisible 입니다.

 

보통의 상황에서는 한번만 import해오면 되기때문에 freezeOnceVisible 기본값은 true입니다.

 

(해당 컴포넌트에 애니메이션이 있기때문에 viewport를 벗어났다가 다시 진입시 애니메이션이

진행되길원한다면 false)

 

import { ReactChild, useRef } from "react";
import useIntersectionObserver from "../hook/useIntersectionObserver";

export default function DyanamicSection({
  bg,
  num,
  prefetch,
  freezeOnceVisible,
  children,
}: {
  bg: string;
  num: string;
  prefetch?: boolean;
  freezeOnceVisible?: boolean;
  children: ReactChild;
}) {
  const ref = useRef<HTMLDivElement | null>(null);
  const entry = useIntersectionObserver(ref, {
    freezeOnceVisible,
    prefetch,
  });
  const isVisible = !!entry?.isIntersecting;
  return (
    <div
      className={`section${num}`}
      ref={ref}
      style={{ background: bg, height: "100vh" }}
    >
      {(isVisible || prefetch) && children}
    </div>
  );
}

num은 해당 섹션에 대한 구분값을 주기위해 임의로 넣은 값이고, 100vh역시 스크린을 꽉채워

 

테스트하기위해 임의로 준 값입니다.

 

해당 컴포넌트가 노출되었는지에 대한 값은 isVisible입니다.

 

이제 해당컴포넌트를

import dynamic from "next/dynamic";
const TestComponent = dynamic(() => import("../src/components/TestComponent"));
//...
<DyanamicSection bg={"skyblue"} num={"1"}>
  <TestComponent />
</DyanamicSection>
//...

위와같이 사용해줍니다. dynamic import를 활용할 컴포넌트를 위에서만든

 

DyanamicSection의 children으로 넣어줍니다.

 

위의 hook과 DyanamicSection 컴포넌트를 6개정도 배치하여 느린 3G환경에서 테스트를 해보았습니다.

dynamic import만 활용했을때
viewport에 따른 dynamic import만 활용했을때

완료시간은 큰 차이가 없지만 DOMContentLoaded시간은 1초정도 차이가 나고,

 

불러오는 데이터량도 viewport에 따른 처리를 했을때 더 적은것을 확인할 수 있습니다.

(컴포넌트 용량자체가 크면 클수록 더 체감이 커질겁니다)

 

위의 작업에 대한 repo는 https://github.com/kimik-hyum/viewport-dynamic-import 입니다. 

 

문제가 되는부분이나 퍼포먼스를 올릴수있는 부분에 대한 피드백 환영합니다!

'Front-End' 카테고리의 다른 글

yarn berry로 workspace(monorepo) 구성하기  (3) 2021.09.20
[Typescript] Generic  (0) 2021.07.15
[flutter] flutter의 상태관리 라이브러리 GetX  (1) 2021.07.08
[flutter] 플러터 입문  (0) 2021.07.01
[React Native Web] 에러 정리  (7) 2021.06.01

댓글