React Query 무한스크롤 기능 구현하는 방법 (Feat. useInfiniteQuery)

김혜림·2025년 2월 24일
0

react

목록 보기
13/15

이 글은 React Query V5 를 이용하여 무한 스크롤을 구현하는 방법이다.
React Query V4는 initialPageParam 이 없어 pageParam에 직접 initialPageParam을 설정해주어야 하며, 이 외의 개념은 동일함.

목록 데이터를 불러올 때 무한스크롤을 사용하는 경우가 종종 있다.

프로젝트에 React Query를 도입했다면, useInfiniteQuery Hook을 사용하면 큰 도움이 된다.

개념 정리 1 - useInfiniteQuery 설정하기

  • initialPageParam : 제일 처음 API를 호출할 때 queryFn이 받을 파라미터의 값이다. GET 요청인 경우 pageParam에 API URL을 전달하고, queryFn에서 pageParam 파라미터를 받아 적절하게 API를 요청한다.

  • getNextPageParam : queryFn이 실행되어 Promise의 resolve값을 받고 나면 getNextPageParam함수가 실행된다.
    이 함수의 인자는 바로 전에 호출된 queryFnresolve 응답 값이다. (Promise를 기다려서 받은 응답 값이라는 뜻임, 아래 코드 예시에서의 lastPage)
    이 파라미터 값을 이용해서 앞으로 호출할 수 있는 데이터가 더 남았는지 안남았는지를 판단해야 한다.
    남아있는 데이터가 없으면 getNextPageParamundefined를 리턴해야 한다.
    남아있는 데이터가 있으면 다음에 실행될 queryFn에 전달할 pageParam을 리턴해야 한다.

useInfiniteQuery Hook 예시

import { useInfiniteQuery } from "@tanstack/react-query";

import httpClient from "apis/networks/HttpClient";
import queryKeyFactory from "apis/query_config/queryKeyFactory";

const LIMIT = 10;

export const useGetComment = (articleId: number) => {
	const initialUrl: string = `/api/v1/article/${articleId}/comments?limit=${LIMIT}`;
	
    const {
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    isFetching,
    isError,
    data,
  } = useInfiniteQuery({
    queryKey: queryKeyFactory.comment({ articleId: articleId }).queryKey,
    queryFn: ({ pageParam }): Promise<GetCommentResType> => {
         return httpClient.get(pageParam);
    },
    initialPageParam: initialUrl,
    getNextPageParam: (lastPage) => {
      if (!lastPage.data.cursor && typeof lastPage.data.cursor !== "number") {
        return undefined;
      } else {
        return `api/v1/article/${articleId}/comments?cursor=${lastPage.data.cursor}&limit=${LIMIT}`;
      }
    },
  });
}

useInfiniteQuery 훅 사용하기

  • IntersectionObserver 등을 이용해서 무한 스크롤의 트리거를 만들어주어야 한다.

  • 무한스크롤 이벤트 트리거가 발생했을 때, useInfiniteQuery Hook의 hasNextPage값이 true이면 fetchNextPage를 호출하면 된다.
    hasNextPage 값은 useInfiniteQuery Hook의 getNextPageParam 함수의 결과가 undefined일 때 false가 된다.

  • useInfiniteQuery에서 isLoadingqueryFn이 처음 로드될 때 true이며, isFetcing은 첫 로드 이후 다음 페이지를 로드할 때 true가 된다.

  • useInfiniteQuerydata는 이중 배열 구조이다.
    data에는 pages 배열이 있으며, queryFn의 결과 한 개가 pages의 배열 아이템이 된다. 그래서 화면에 무한 스크롤 아이템을 랜더링 할 때는 map을 이중으로 돌려야 한다.

useInfiniteQuery 훅 사용 예시

const Comment = ({ articleId }: { articleId: string }) => {
	const observer = useRef<IntersectionObserver | null>(null);
    const lastItemRef = useRef<HTMLDivElement>(null);
    
    const { data, isLoading, isError, hasNextPage, fetchNextPage } =
    useGetComment(Number(articleId));
    
    useEffect(() => {
    const handleObserver = (entries: IntersectionObserverEntry[]) => {
      const [entry] = entries;
      if (entry.isIntersecting && hasNextPage) {
        fetchNextPage();
      }
    };

    const options: IntersectionObserverInit = {
      root: null, // 뷰포트를 기준으로 감지
      rootMargin: "0px",
      threshold: 1.0,
    };

    observer.current = new IntersectionObserver(handleObserver, options);

    const lastItem = lastItemRef.current; // ref 값을 로컬 변수로 저장

    if (lastItem) {
      observer.current.observe(lastItemRef.current);
    }

    return () => {
      if (observer.current && lastItem) {
        observer.current.unobserve(lastItem); // 컴포넌트 언마운트 시 관찰 종료
      }
    };
  }, [hasNextPage, fetchNextPage]);
  
  return (
  	<div className={classes.comment_list}>
    	 {!isLoading && data?.pages[0].data.items.length === 0 && (
          <div className={classes.no_comment}>아직 댓글이 없습니다.</div>
        )}
        {!isLoading &&
          !isError &&
          !!data &&
          data.pages.map((page) =>
            page.data.items.map((commentItem: CommentItemType) => {
              return (
                <CommentItem
                  key={commentItem.commentId}
                  commentItem={commentItem}
                />
              );
            }),
          )}
      </div>
      <span ref={lastItemRef} />
    </div>
  );
}

결과 화면

profile
개발 일기입니다. :-)

0개의 댓글