[React-query] Infinite Query

SoShy·2024년 5월 15일

React-Query

목록 보기
12/13
post-thumbnail

Infinite Query


버튼을 통한 무한 스크롤을 구현하고 싶을 때는, React-query에서 제공하는 useInfiniteQuery를 이용하면 쉽게 구현이 가능하다.

우선 다음과 같이 기본 틀을 만들어주었다.

...

return (
  <>
    <div>
      <form onSubmit={handleSubmit}>
        <textarea
          name='content'
          value={content}
          onChange={handleInputChange}
        />
        <button
          disabled={uploadPostMutation.isPending || !content}
          type='submit'
        >
          업로드
        </button>
      </form>
    </div>
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{`${post.user.id}: ${post.content}`}</li>
        ))}
      </ul>
      <div>
        <button>더 불러오기</button>
      </div>
    </div>
  </>
);

🏷️ useInfiniteQuery()


더 불러오는 기능을 만들어주기 위해, 기존에 사용하던 useQuery()useInfiniteQuery()로 변경해주자.

이 때, useQuery()와는 달리, useInfiniteQuery()에서는 initialPageParamgetnextPageParam 옵션을 설정해주어야 한다.

또한, pagination과는 다르게, 페이지 별로 데이터를 별도로 저장할 필요가 없기 때문에, 쿼리키는 ['post']로 지정해주었다.

import {
  // ...
  useInfiniteQuery,
} from '@tanstack/react-query';

//

const {
  data: postsData,
  isPending,
  isError,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => getPosts(pageParam, PAGE_LIMIT),
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
    lastPage.hasMore ? lastPageParam + 1 : undefined,
});

useQuery()에서는 data가 백엔드에서 받아온 하나의 페이지 정보만 담고 있지만, useInfiniteQuery()에서는 data.pages에 배열의 형태로 모든 페이지의 정보를 담고 있게 된다.

맨 처음 첫 번째 페이지의 데이터를 받아오면 data.pages 배열의 0번 인덱스에 해당 데이터가 저장되고, 두 번째 페이지로 넘어가면 1번 인덱스에 데이터가 저장되는 것이다.

하나의 배열에 여러 페이지의 데이터들이 모두 담겨있으므로, 모든 데이터를 한 번에 화면에 보여줄 수 있으므로, 이를 이용해 더 불러오기는 구현할 수 있다.

이 데이터들은 ['posts']라는 하나의 쿼리 키로 캐싱된다.



🏷️ pageParam


initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
  lastPage.hasMore ? lastPageParam + 1 : undefined,

initialPageParam으로는 초기 페이지 설정값을 정하게 되는데, 여기서 사용하는 API에서는 0 페이지가 초기 페이지라는 가정 하에, 값을 0으로 설정하였다.

getNextPageParam() 함수는 다음 페이지의 설정값을 정할 수 있으며, lastPage, allPages, lastPageParam, allPageParams를 파라미터로 전달받는다.

lastPage는 현재까지 중 가장 마지막 페이지의 데이터가 전달된다. 만약, 현재 2 페이지에 해당하는 데이터까지 받아왔다면, 2 페이지의 데이터가 lastPage로 전달되는 것이다.

allPages로는 모든 페이지의 데이터가 전달된다.

lastPageParam은 현재까지 중 가장 마지막 페이지의 설정값을 의미한다. 현재 2 페이지까지 받아 왔다면 lastPageParams은 2가 된다.

allPageParams는 모든 페이지의 각각의 페이지 설정값을 가진다.

getNextPageParam()에서는 파라미터로 받은 값들의 정보를 이용해 그 다음 페이지 값인 pageParam을 리턴해야 한다.

만약, 현재 0 페이지라면 그 다음 값은 1이 되어야 한다.

따라서, 0 페이지의 데이터에서 hasMore 값을 확인하여 true인 경우, lastPageParam의 값인 0에 1을 더한 값인 1을 리턴해주면 되는 것이다.

hasMore 값이 false인 경우, undefinednull을 리턴해주면 된다.

pageParam 값은 쿼리 함수의 파라미터로 전달되기 때문에, 이 값을 이용하여 백엔드에 해당 페이지에 해당하는 데이터를 요청할 수 있다.

queryFn: ({ pageParam }) => getPosts(pageParam, PAGE_LIMIT)


🏷️ 다음 페이지 불러오기


다음 페이지를 불러오려면, useInfiniteQuery()의 리턴 값 중 하나인 fetchNextPage() 함수를 이용하면 된다.

fetchNextPage() 함수를 실행하면, getnextPageParam() 함수의 리턴 값이 undefinednull이 아닌 경우, 해당 리턴 값을 쿼리 함수의 pageParam으로 전달하여, 그 다음 페이지의 데이터를 가져온다.

따라서, 버튼의 onClick() 함수로 fetchNextPage() 함수를 등록해주면 된다.

const {
  data: postsData,
  isPending,
  isError,
  fetchNextPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => getPosts(pageParam, PAGE_LIMIT),
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
    lastPage.hasMore ? lastPageParam + 1 : undefined,
});

// ...

return (
  ...
    <div>
      <button onClick={fetchNextPage}>더 불러오기</button>
    </div>
  ...
);


🏷️ 페이지 데이터를 구조에 맞게 정리하기


postsData 안에는 pages라는 배열에 모든 페이지의 포스트 데이터가 담겨있다.

따라서, pages 배열을 Array.map() 함수를 통해 순회하면서, 각 페이지에 해당하는 포스트 데이터를 모두 보여주도록 만들 수 있다.

// ...

const postsPages = postsData?.pages ?? [];

return (
    ...
        <div>
      <ul>
        {postsPages.map((postPage) =>
          postPage.results.map((post) => <Post key={post.id} post={post} />)
        )}
      </ul>
    <div>
    ...
);
function Post({ post }) {
  return (
    <li key={post.id}>
      {post.user.name}: {post.content}
    </li>
  );
}

export default Post;


🏷️ 버튼 비활성화 처리하기


더 이상 불러올 데이터가 없거나 다음 데이터를 불러오는 중일 때는 버튼을 비활성화 시키는 것이 좋다.

이는 useInfiniteQuery()의 리턴 값 중 hasNextPageisFetchingNextPage를 이용하면 간단하게 구현할 수 있다.

const {
  data: postsData,
  isPending,
  isError,
  hasNextPage,
  fetchNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => getPosts(pageParam, PAGE_LIMIT),
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
    lastPage.hasMore ? lastPageParam + 1 : undefined,
});

// ...

return (
    ...
        <button
      onClick={fetchNextPage}
      disabled={!hasNextPage || isFetchingNextPage}
    >
      더 불러오기
    </button>
    ...
);
profile
프론트엔드 개발자가 되기 위해 노력 중인 새싹🌱 입니다.

0개의 댓글