데이터 패칭 라이브러리 - TanStack Query (6. 페이지네이션)

eeensu·2023년 8월 6일
0
post-thumbnail

Prefetching

Prefetching은 웹 애플리케이션이나 웹 페이지의 사용자 경험을 향상시키기 위해 데이터를 미리 불러오는 기술을 말한다. 주로 사용자가 페이지를 요청하기 전에 필요한 데이터를 미리 가져와서 페이지 로딩 속도를 향상시키고 사용자 경험을 부드럽게 만드는 데 사용된다.

TanStack Query에서는 prefetchQuery() 함수를 사용하여 데이터를 미리 가져오는 것이 가능하다. 이 함수는 미리 데이터를 가져올 때 사용자가 페이지로 이동하지 않았더라도 데이터를 캐시하고 유지한다. 이렇게 하면 사용자가 해당 데이터를 요청할 때 실제 요청보다 빠르게 데이터를 제공할 수 있다.

const BlogPostList = () => {
  const queryClient = useQueryClient();

  // 컴포넌트가 마운트되면 미리 블로그 포스트 목록 데이터를 가져온다.
  
  useEffect(() => {
    // prefetchQuery를 사용하여 'posts' 쿼리를 미리 불러온다.
    queryClient.prefetchQuery({
        queryKey: ['todos'],
        queryFn: fetchTodos,
    });
  }, []);

  return (
    <div>
      {/* 블로그 포스트 목록을 렌더링하는 코드 ... */}
    </div>
  );
};



페이지네이션

페이지네이션을 구현하는 것은 데이터를 여러 페이지로 나누어 가져와 효율적으로 표시하는 방법이다. 페이지네이션은 대량의 데이터를 한 번에 로드하지 않고 필요한 만큼 조금씩 로드하여 사용자 경험을 개선하는 데 사용된다.

하지만 Pagenation 기능이 적용된 쿼리는 한가지 오류가 발생할 수 있다. 바로 UI 점프이다. 각각의 새 페이지는 완전히 새로운 쿼리처럼 취급되기 때문에 UI는 successloading 상태를 왔다갔다 한다. 이러한 현상은 최적화와 거리가 멀며, TanStack Query는 이를 해결하기 위해 keepPreviousData 옵션을 지원한다. 이 옵션은 새 데이터가 올 때까지 이전 데이터의 형태를 유지하는 옵션이며 페이지네이션에 최적화된 기능이다. 페이지네이션을 사용하는 커스텀 훅을 다음과 같이 예로 들어보겠다.

export interface AxiosInterface {
    projects: Project[];				// 페이지네이션을 통해 불러올 데이터의 타입
    hasMore: boolean;					// 더 불러올 데이터가 있는지
    nextCursor: number;			
}

// 인자로 받아오는 page는 컴포넌트에서 사용하는 페이지 번호이다.
export const useProjectList = (page: number) => {
    const { data, isError, error, isFetching, isLoading, isPreviousData } = useQuery<AxiosInterface>({
        queryKey: ['pages', page],
        queryFn: () => getProjects(page),		// 받아온 페이지를 바탕으로 데이터를 가져옴
        keepPreviousData: true
    })

    return {
        data, isError, error, isFetching, isLoading, isPreviousData
    };
};

  • isPreviousData
    keepPreviousData가 준비되고, 이전 쿼리로부터 온 데이터가 리턴되면 true가 된다.

이후 컴포넌트에선 다음과 같이 작성한다.

const ReactQueryExample3: FC = () => {
    
    const [page, setPage] = useState<number>(0);

    const { data , error, isFetching, isPreviousData, isLoading } = useProjectList(page);

    return (
        <div>
            {isLoading ? (
            <div>Loading...</div>
            ) : error instanceof Error ? (
            <div>Error: {error.message}</div>
            ) : (
            <div>
                {data?.projects.map(project => (
                    <p key={project.id}>{project.name}</p>
                ))}
            </div>
            )}
            <span>Current Page: {page + 1}</span>
            <button
                onClick={() => setPage(prev => Math.max(prev - 1, 0))}
                disabled={page === 0}
            >
                Previous Page
            </button>{' '}
            <button
                onClick={() => {
                    if (!isPreviousData && data?.hasMore) {
                        setPage(prev => prev + 1)
                    }
                }}
                disabled={isPreviousData || ! data?.hasMore}
            >
                Next Page
            </button>
        {isFetching ? <span> Loading...</span> : null}{' '}
      </div>
    );
};

해당 결과처럼 나온다.




useInfiniteQuery

무한 스크롤 형태의 페이지네이션을 구현하는데 사용된다. 페이지네이션을 사용하는데 있어서 데이터를 일정량씩 가져와서 스크롤을 할 때마다 추가 데이터를 불러오는 패턴을 구현할 때 유용하다. 이를 통해 사용자가 스크롤을 내리면 새로운 데이터를 자연스럽게 불러올 수 있기에 페이지네이션보다 더 최신 트렌드의 기능이다.

인피니티 스크롤을 사용하는 커스텀훅을 먼저 만든다.

export const useInfiniteExample = () => {
  const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } 
  = useInfiniteQuery<AxiosInterface>({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    
    // 다음 페이지에 필요한 파라미터를 추출하는 역할을 한다. 
    // 이를 통해 다음 페이지를 가져오는 데 필요한 정보를 지정할 수 있다.
    getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,    
  });

  return {
    data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status
  };
};

커스텀 훅을 사용하는 컴포넌트를 만든다.

const ReactQueryExample4: FC = () => {

    const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteExample();

    const customError = error as CustomError;

    return status === 'loading' ? (
        <p>Loading...</p>
    ) : status === 'error' ? (
        <p>Error: {customError?.message}</p>
    ) : (<>
        {data?.pages.map((group, i) => (
            <Fragment key={i}>
                {group.projects.map((project) => (
                    <p key={project.id}>{project.name}</p>
                ))}
            </Fragment>
        ))}
        <div>
            <button
                onClick={() => fetchNextPage()}
                disabled={!hasNextPage || isFetchingNextPage}
            >
                {isFetchingNextPage ? 
                    'Loading more...' : 
                    hasNextPage ? 'Try Load more!' : 
                    'Nothing to Load'}
            </button>
        </div>
    </>)
};

결과는 아래와 같다.

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글

관련 채용 정보