ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • custom Infinite Scrolling hooks (with tanstack Query in Next.js)
    FrontEnd/React.js 2024. 9. 3. 14:59

    오랜만에 무한 스크롤링 구현하면서 기록을 남겨봄

    두가지 커스텀 훅을 만들 예정

    1. 리액트 쿼리에서 제공하는 useInfiniteQuery를 활용한 커스텀 훅 ->이하 useQuery( 아래는 반환 값 ) -> useInfiniteQuery 

    {data, fetchNextPage, hasNextPage, isFetchingNextPage, status}

     

    2.  바닥 ref가 InView인지 확인하여 조건 만족시 fetchNextPage 발동 할 useInfiniteScroll 

     

    1. useQuery 매개변수로는 아래 요인들 ( 카테고리에서 날짜,지역,태그에 따라 보여지는 게시물들이 다름)

    {category,searchDate,searchRegions, searchTags}

    일단 react-query 에서 제공 받을 값들부터 알아보자 

     

    • data:
      • 서버로부터 받은 응답 데이터를 포함하는 객체
      • *** 무한 쿼리의 경우, 이 객체는 pages 배열을 포함 
      • *** 각 page는 개별 요청에 대한 응답 데이터
        • 예: data.pages[0]은 첫 번째 페이지의 데이터
    • fetchNextPage:
      • 다음 페이지의 데이터를 가져오는 ***함수
      • 사용자가 "더 보기" 버튼을 클릭하거나 스크롤의 끝에 도달했을 때 호출
      • 이 함수를 호출하면 새로운 페이지의 데이터가 ****(기존 data.pages 배열에 추가)
    • hasNextPage:
      • 불리언 값으로, ***더 가져올 데이터가 있는지를 나타냄.
      • true면 더 많은 페이지가 있다는 뜻이고, false면 모든 데이터를 가져왔다는 뜻
      • 이 값은 주로 getNextPageParam 옵션의 반환값에 따라 결정
    • isFetchingNextPage:
      • 불리언 값으로, 현재 다음 페이지를 ***가져오고 있는 중인지를 판단
      • true일 때 로딩 인디케이터를 표시하는 데 유용
    • status:
      • 쿼리의 현재 상태를 나타내는 문자열
      • 'loading': 초기 데이터를 가져오는 중 -> 맨 처음 데이터 불러올 때라는 뜻
      • 'pending'
        • Tanstack Query v5에서 도입된 새로운 상태
        • loading을 대체
        • 쿼리가 아직 데이터를 가져오지 않았거나 가져오는 중임을 나타냄
        • 캐시된 데이터가 있더라도, 백그라운드에서 refetch가 진행 중이면 'pending' 상태가 될 수 있음
      • 'error': 데이터 가져오기 실패
    • isFetchingNextPage VS status.pending
      • isFetchingNextPage: 초기 데이터 **이후 추가 페이지를 가져올 때 사용
      • status pending: 초기 데이터를 가져올 때 또는 전체 쿼리가 다시 실행될 때 사용

    아래는 인피니티쿼리훅에 넘겨줄 인자들 

    useInfiniteQuery({
          queryKey: [category, searchDate, searchRegions, searchTags],
          queryFn: ({ pageParam }) => getCategoryList(pageParam),
          initialPageParam: 0,
          getNextPageParam: (lastPage) => lastPage?.nextCursor,
    });

     

    1. queryKey:
      • 쿼리를 식별하는 고유한 키
      • 배열 형태로 지정
      • 이 키를 기반으로 쿼리 결과가 캐시되고 관리됨. (쿼리 메모리에서 끄집어온다능)
    2. queryFn:
      • 데이터를 가져올 패칭 함수
      • 이 함수는 pageParam을 인자로 받아 페이지네이션을 구현함 -> 위에서 언급한 data.pages[num]
    3. initialPageParam:
      • 첫 번째 페이지를 가져올 때 사용할 초기 페이지 파라미터 값
      • 이 값은 queryFn의 첫 번째 호출 시 pageParam으로 전달
      • default undefined
    4. getNextPageParam:
      • 다음 페이지의 파라미터를 결정하는 함수
      • 현재 페이지 데이터와 모든 페이지 데이터를 인자로 받음
      • 이 함수의 반환값이 다음 queryFn 호출의 pageParam으로 사용됨
      • undefined를 반환하면 더 이상 페이지가 없다고 간주

    ===>

      const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
        useInfiniteQuery({
          queryKey: [category, searchDate, searchRegions, searchTags],
          queryFn: ({ pageParam }) => getCategoryList(pageParam),
          initialPageParam: 0,
          getNextPageParam: (lastPage) => lastPage?.nextCursor,
        });
    
      return {
        data,
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage,
        status,
      };

    data => 화면에 보여줌

    fetchNextPage => 무한 스크롤을 통해 바닥 찍으면 발동 (단, hasNextPage가 true일 시 ) 

    isFetchingNextPage => 바닥을 찍고 추가로 데이터를 가져오는 상황이라면 loader 애니메이션을 보여줄 거임

    status => error / pending을 통해 에러 또는 초기 로딩시 스켈레톤 보여줄 용도

     

    2.  바닥 ref가 InView인지 확인하여 조건 만족시 fetchNextPage 발동 할 useInfiniteScroll 

    1. bottomRef 를 따로 선언하여 쿼리 데이터중 가장 마지막 요소에게 할당

    2. 옵저버를 통해 inview 이면 fetchNextPage() 실행 

     *** 트러블 슈팅:

    • 이상하게 클라이언트가 동작(이벤트 발생 같은)이후에야 해당 inView가 관찰됨 
      • page.tsx에서 'use client'를 선언하는 것이 클라이언트 렌더링 버그가 생기나 해서 단위 컴포넌트로 쪼갠 후 그 안에서 클라이언트 렌더링을 명시해봄 -> 이전과 똑같음
      • next.js만의 무슨 버그인건가? -> 임시 페이지 생성 후 관찰 했을 떄는 잘됨 
      • 그렇다면 ref 할당 시점이 늦어지는건가 싶어서 하이드레이션 지연 시켜질만한 요소들 찾아봄 -> useQuery 훅 동작을 제외했더니 관찰 잘 됨 -> 근데 이 훅 사용자체가 렌더링에 문제를 만들어 ref 할당이 늦어지고 inView 관찰이 안되는건 말이 안됨 =====>  status에 따라 아래와 같이 코드를 짰을 때 관찰이 안됐던 거였음 (왜냐면 아직 서버에 직접 연결안 한 상태라 껄껄)
    {status === "pending" ? (
            <CategoryList.skeleton />
          ) : (
            <CategoryList
              bottomRef={bottomRef}
              categoryType={(category as "popup") || "exhibition"}
            />
          )}
    • 마지막 카드가 시야에 들어오면 fetchNextPage를 실행시키게 짤려는데 관찰되는 마지막 카드가 처음에 할당된 ref로 고정박혀짐
      • 화면에 표출되는 카드에 useRef로 할당된 ref값을 넘겨준 뒤 이것이 시야에 들어오면 data 패칭이 일어나서 카드들은 증가할 것. bottomRef를 length-1 === idx인 카드에 할당해서 리렌더링시 자동으로 bottomRef가 추가로 할당된 마지막 카드를 가르킬 것이라 생각함. --> useRef 특성이 리렌더링이 일어나더라도 값이 변하지 않는다는(ReadOnly니까..) 특성을 잊은 잘못
      • 이것에 대책으로 카드 리스트 바닥에 heigth 1인 투명한 박스에 bottomRef를 줘서 해당 박스가 시야에 들어오면 fetchNextPage가 일어나도록 변경
    import { useCallback, useEffect, useRef, useState } from "react";
    
    type IUseInfiniteScroll = {
      loadMoreFunC: () => void;		// useInfiniteQuery의 fetchNextPage
      shouldMore: boolean;			// useInfiniteQuery의 hasNextPage
    };
    const useInfiniteScroll = ({
      shouldMore,
      loadMoreFunC,
    }: IUseInfiniteScroll) => {
      const bottomRef = useRef<HTMLInputElement>(null);
      const [inView, setInView] = useState(false);
    
      const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => {
        const target = entries[0];
        setInView(target.isIntersecting);
      }, []);
    
      useEffect(() => {
        const ob = new IntersectionObserver(handleObserver, { threshold: 1 });
        if (bottomRef.current) ob.observe(bottomRef.current);
        return () => {
          if (bottomRef.current) ob.unobserve(bottomRef.current);
        };
        // obRef.current.
      }, [bottomRef]);
    
      useEffect(() => {
        if (inView && shouldMore) {
          loadMoreFunC();
        }
      }, [inView, shouldMore]);
      return { inView, bottomRef };
    };
    
    export default useInfiniteScroll;

     

Designed by Tistory.