Frontend/React

[React] Infinite Scroll 구현하기 : Intersection Observer API

돌잡이개발자 2022. 2. 14. 04:22

인피니티 스크롤이란 컴포넌트를 한 번에 불러오는 것이 아니라 스크롤 하면서새 컴포넌트가 렌더되는 것을 말한다.

탐색 위주의 모바일 친화적이며, 컨텐츠를 소비하는 속성을 가진 웹사이트라면 인피니트 스크롤을 적용하는 것이 쾌적한 사용자 경험을 제공하는 데 도움이 된다.

 

Intersection Observer API 

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

 

Intersection Observer API는 브라우저의 뷰포트와 대상 요소의 교차점을 관찰하며 변경 사항을 비동기적으로 관찰하는 방식을 제공한다. 쉽게 말하면 우리는 관찰 대상 요소를 정하고 뷰포트에 그 요소가 보여졌을 때 Intersection Observer를 통해 관찰할 수 있는 것이다. 이를 통해 지정한 요소가 화면에 나타나면 컴포넌트를 렌더링하도록 만들 수 있다. Intersection Observer는 다음과 같은 상황에서 유용하게 쓰일 수 있다.

 

  • 페이지를 스크롤할 때 이미지 또는 기타 콘텐츠를 lazy-loading위해
  • 스크롤하면서 점점 더 많은 콘텐츠가 로드 및 렌더링되는 무한 스크롤 웹 사이트 구현하기 위해
  • 광고 가시성을 확인하여 수익 계산을 위해
  • 사용자가 결과를 볼 수 있는지 여부에 따라 작업 또는 애니메이션 프로세스를 수행할지 여부를 결정하기 위해

 

사용법

Intersection Observer 옵션

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);
  • root

대상의 가시성을 확인하기 위한 뷰포트로 사용되는 요소입니다. 관찰 대상의 상위 항목이어야 합니다. 지정되지 않았거나 브라우저 뷰포트인 경우 기본값 null입니다.

 

  • rootMargin

루트 주변의 margin 속성.

 

  • threshold

관찰자의 콜백이 실행되어야 하는 대상 가시성의 백분율을 나타내는 단일 숫자 또는 숫자 배열입니다. 가시성이 50% 표시를 통과할 때만 감지하려면 0.5 값을 사용할 수 있습니다. 

 

Intersection Observer 타겟팅

let target = document.querySelector('#listItem');
observer.observe(target);

관찰 대상 요소 지정하기 

 

let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

대상이 지정한 임계값을 충족할 때마다 IntersectionObserver 콜백이 호출됩니다. 콜백은 IntersectionObserverEntry객체 목록과 관찰자를 수신합니다.

 

코드 활용

내가 프로젝트에 활용한 무한 스크롤 코드는 다음과 같다.

const SHOW_QUIZS_QUERY = gql`
	...
`;

export default function QuizList() {
  const isQuizLoadEndVar = makeVar(false);
  const { loading, data, refetch, fetchMore } = useQuery<showQuizs>(
    SHOW_QUIZS_QUERY,
    {
    //한 번에 15개씩 불러오기
      variables: { take: 15 },
      onCompleted: () => {
        isQuizLoadEndVar(false);
      },
    }
  );  
  
  //대상요소 선택하기
  const loaderRef = useRef<any>();
  
  const handleObserver = useCallback(
    async (entries) => {
      const isQuizLoadEnd = isQuizLoadEndVar();
      if (isQuizLoadEnd) {
        return;
      }
      const target = entries[0];
      //화면에 대상 요소가 겹친다면
      if (data?.showQuizs && target.isIntersecting) {
        const lastId = data.showQuizs[data.showQuizs.length - 1].id;
        //더 불러오기
        const more = await fetchMore({
          variables: {
            lastId,
          },
        });
        //해당 요소가 마지막이면
        if (more?.data?.showQuizs?.length === 0) {
          //더 이상 불러오지 않게 한다
          isQuizLoadEndVar(true);
        }
      }
    },
    [data]
  );

  useEffect(() => {
  //관찰자의 콜백이 호출되는 상황을 제어하는 옵션
    const option = {
      root: null,
      rootMargin: "0px",
      threshold: 0,
    };
    //관찰자를 생성하고 handleObserver라는 콜백함수와 option이라는 인수를 받는다
    const observer = new IntersectionObserver(handleObserver, option);
    //대상 요소가 보이면 대상요소를 관찰한다
    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }
  }, [handleObserver]);

  return (
    <div className="grid gap-4 pb-4 sm:grid-cols-2 md:grid-cols-3 ">
        {data?.showQuizs?.map((post, index) => {
            return <Quiz key={index} post={post} />
        })}
        //Intersection Observer가 관찰할 대상 요소를 마지막에 빈 <div>로 넣기
        <div ref={loaderRef}></div>
    </div>
  );
}

 

느낀 점

이렇게나 간단하게 설명했지만 사실 Intersection Observer API는 MDN에 어마어마하게 방대하게 소개되어있다. (긴글주의)

하나씩 읽어보고 정리하면서 다시 한 번 공식 문서의 중요성을 깨달았다.

 

참고문서

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

반응형