프로젝트

react-query useInfiniteQuery로 inifinite scrolll구현하기 & useIntersectionObserver 커스텀훅

Jenner 2024. 3. 30. 22:10

 

 

 

useInfiniteQuery

 

 

const { data: receiptData, fetchNextPage } = useInfiniteQuery<
    ReceiptsReturnType,
    unknown,
    InfiniteData<ReceiptsReturnType>,
    [_1: string, _2: string, _3: string],
    unknown
  >({
    queryKey: ['receipt', 'monthly', yearMonth],
    queryFn: ({ pageParam }) =>
      getReceiptsByMonth({
        pageParam,
        YM: yearMonth || dayjs().format('YY.MM'),
      }),
    initialPageParam: 1,
    getNextPageParam: (data) => {
      return data.nextCursor;
    },
    staleTime: 10 * 60 * 1000,
  });

 

 

이것의 Return Type은 다음과 같다 

 

 

interface InfiniteData<TData, TPageParam = unknown> {
    pages: Array<TData>;
    pageParams: Array<TPageParam>;
}

 

 

그런데 이런 Type에 맞춰서 백엔드 Api에서 return을 해줘야 하나 했는데 그게 아니다. 

useInfiniteQuery의 Type을 정의하다보면 

첫번째와 세번째 타입을 보자면 첫번째 타입에는 ReceiptsReturnType이고 

세번째는 InifiniteData의 제네릭에 ReceiptsReturnType을 전달해 주고 있다. 

그리고 해당 InifiniteData의 타입은 위와 같이 전달된 첫번째 제네릭 인수가 어레이 형태로 

pages의 값으로 들어가는 것을 확인할 수 있었다. 

 

그러니까 들어온 return 데이터를 자동으로 어레이 형태로 넣어주고 있는 것이다. 

그래서 그거에 맞춰서 이전의 렌더링 방식도 바꿔주었다. 

 

이전엔 아래와 같았다면, 

 

receiptData.receipts.map((data: ReceiptArrType) => (

 

useInfiniteQuery를 사용하였을 때는 아래와 같다. 

 

 

 receiptData.pages.map((page: ReceiptsReturnType) =>
          page.receipts?.map((data: ReceiptArrType) => (

 

 

그러니까 array가 하나 더 들어간 셈이다. 

이걸 토대로 아마 다음 데이터를 불러오게 되는 것이고, 

그걸 어레이로 넣어주어 새로 렌더링 되는 것이 아닐까 추측해본다

 

 

useIntersectionObserver 커스텀훅

 

import { useEffect } from 'react';

interface useIntersectionObserverProps {
  root?: null;
  rootMargin?: string;
  threshold?: number;
  target: HTMLDivElement | null;
  onIntersect: IntersectionObserverCallback;
}

const useIntersectionObserver = ({
  root,
  rootMargin = '0px',
  threshold = 1,
  target,
  onIntersect,
}: useIntersectionObserverProps) => {
  useEffect(() => {
    if (!target) return;
    const observer: IntersectionObserver = new IntersectionObserver(
      onIntersect,
      { root, rootMargin, threshold },
    );
    observer.observe(target);

    return () => observer.unobserve(target);
  }, [onIntersect, root, rootMargin, target, threshold]);
};

export default useIntersectionObserver;

 

 

root : 는 관찰하는 화면. 전달되지 않으면 뷰포트가 됨

rootMargin : 관찰 대상의 마진을 얼마나 둘 것인가 기본값 0. 단위 꼭 줘야 함. 

threshold : 0~1의값 얼마나 노출 되었을 때 옵저버가 실행 될 것인지 

targt : 관찰 대상 해당 노드가 화면에 노출되면 옵저버가 실행

onIntersect : 옵저버 실행함수

 

useIntersectionObserver로 실행하면 인수로 준 옵션값들이 변경되면 useEffect가 실행되고 observe가 시작됨. 

intersectionObserver로 observer라는 인스턴스를 만들고 인스턴스의 메서드인 observe와 unobserve를 활용하여 

관찰을 시작하거나 메모리 누수방지를 위해 클린업함. 

 

onIntersect를 외부에서 받을 수 있게하여

외부에서 실행할 함수를 정의할 수 있게 되어있음 

 

 

구현시 오래 걸렸던 점 

 

처음에 렌더링 될 시에는 

api통신으로 가져온 데이터가 없는 상황이므로 

렌더링이 안되어있는데, 

관찰 대상 노드는 먼저 렌더링이 되어있는 상황에서 

계속 노출이 되므로 

옵저버가 계속 실행되어 

렌더링 되기도 전에 모든 데이터들을 혼자 다 불러왔었다. 

 

 

수정한 부분 

 

    if (!isIntersecting) return;

 

이부분을 설정하여 관찰중이 아닐 때는 fetchNextPage()가 실행되지 않도록 방지하였더니 

정상적으로 스크롤을 아래로 내린뒤에 실행되었다.