[React-query] Paginated Query

SoShy·2024년 5월 13일

React-Query

목록 보기
11/13
post-thumbnail

Paginated Query


이번 포스팅에서는 pagination을 구현해보도록 하자.

다음 API 함수는 쿼리 파라미터로 pagelimit을 넘겨주면 해당하는 page의 데이터를 limit 개수만큼 보내주도록 설계되어 있다.

export async function getPosts(page = 0, limit = 10) {
  const response = await fetch(`${BASE_URL}/posts?page=${page}&limit=${limit}`);
  return await response.json();
}

다음으로, useState()를 사용하여 page 변수를 만들어준 후, useQuery()에서 queryKeypage를 추가하여 page 별로 데이터를 캐싱해주었다.

그리고 쿼리 함수 호출 부분에 page를 추가해주었다.

import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getPosts, uploadPost, getUserInfo } from './api';

const PAGE_LIMIT = 3;

function HomePage() {
  // ...

  const [page, setPage] = useState(0);
  const {
    data: postsData,
    isPending,
    isError,
  } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => getPosts(page, PAGE_LIMIT),
  });

  // ...

  const posts = postsData?.results ?? [];

  return (
    <>
      <div>
        {currentUsername ? (
          loginMessage
        ) : (
          <button onClick={handleLoginButtonClick}>codeit으로 로그인</button>
        )}
        <form onSubmit={handleSubmit}>
          <textarea
            name="content"
            value={content}
            onChange={handleInputChange}
          />
          <button disabled={!content} type="submit">
            업로드
          </button>
        </form>
      </div>
      <div>
        <ul>
          {posts.map((post) => (
            <li key={post.id}>
              {post.user.name}: {post.content}
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

export default HomePage;

위 코드를 실행하여 결과를 확인해보면 첫 페이지(page = 0)에 해당하는 세 개의 데이터가 보여질 것이다.

이제 페이지를 변경할 수 있는 버튼을 추가해보자.

React-query 개발자 도구로 posts 데이터를 살펴보면 hasMore라는 값이 있는데, 백엔드에서 그 다음 페이지가 있을 때 hasMore 값을 true로 보내주게 된다.

즉, 이 hasMore 값으로 다음 페이지 버튼의 활성화 여부를 결정할 수 있는 것이다.

import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getPosts, uploadPost, getUserInfo } from './api';

const PAGE_LIMIT = 3;

function HomePage() {
  // ...
  const [page, setPage] = useState(0);
  const {
    data: postsData,
    isPending,
    isError,
  } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => getPosts(page, PAGE_LIMIT),
  });
  
  // ...

  const posts = postsData?.results ?? [];

  return (
    <>
      <div>
        {currentUsername ? (
          loginMessage
        ) : (
          <button onClick={handleLoginButtonClick}>codeit으로 로그인</button>
        )}
        <form onSubmit={handleSubmit}>
          <textarea
            name="content"
            value={content}
            onChange={handleInputChange}
          />
          <button disabled={!content} type="submit">
            업로드
          </button>
        </form>
      </div>
      <div>
        <ul>
          {posts.map((post) => (
            <li key={post.id}>
              {post.user.name}: {post.content}
            </li>
          ))}
        </ul>
        <div>
          <button
            disabled={page === 0}
            onClick={() => setPage((old) => Math.max(old - 1, 0))}
          >
            &lt;
          </button>
          <button
            disabled={!postsData?.hasMore} // hasMore 사용 부분
            onClick={() => setPage((old) => old + 1)}
          >
            &gt;
          </button>
        </div>
      </div>
    </>
  );
}

export default HomePage;

위와 같이 코드를 작성해주면, 마지막 페이지로 갔을 때 다음 페이지 버튼이 성공적으로 비활성화되는 것을 확인할 수 있다.

그런데, 지금 상태에서는, 다음 페이지로 넘어갈 때마다 매번 로딩 메세지가 뜨는 오류가 존재하는데,

이는 새로운 페이지에 해당하는 쿼리를 보낼 때마다 완전히 새로운 쿼리로 인식하기 때문에, 계속 pending 상태가 되기 때문이다.

이를 해결하기 위해, React-query에서는 조금 더 부드러운 UI 전환을 위한 placeholderData를 설정해줄 수 있다.

useQuery()에서 placeholderData 옵션에 keepPreviousData 혹은 (prevData) => prevData를 넣어주면, 페이지가 새로 바뀌더라도 매번 pending 상태가 되지 않고, 이전의 데이터를 유지해서 보여주다가, 새로운 데이터 fetch가 완료되면 자연스럽게 새로운 데이터로 바꿔서 보여주게 된다.

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

const {
  data: postsData,
  isPending,
  isError,
} = useQuery({
    queryKey: ['posts', page],
    queryFn: () => getPosts(page, PAGE_LIMIT),
    placeholderData: keepPreviousData,
});

이제 코드를 실행해보면, 다음 버튼을 눌렀을 때 로딩 메세지가 보이지 않고 이전 페이지의 내용이 그대로 보이다가, 데이터를 모두 받아오면 다음 페이지의 내용으로 바뀌는 것을 확인할 수 있다.

그런데, 여기서도 한 가지 보완할 점이 존재하는데, 다음 버튼을 누르고 새로운 데이터를 받아오는 중간 과정에서 다음 페이지 버튼이 활성화되어 있다는 것이다.

이 때, useQuery()의 리턴 값에서 isPlaceholderData 값을 활용하면, 현재 보이는 데이터가 이전 데이터(placeholderData) 일 때, 다음 페이지 버튼을 비활성화시킬 수 있다.

const {
  data: postsData,
  isPending,
  isError,
  isPlaceholderData,
} = useQuery({
  queryKey: ['posts', page],
  queryFn: () => getPosts(page, PAGE_LIMIT),
  placeholderData: keepPreviousData,
});

...

return (
  ...
    <div>
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{`${post.user.name}: ${post.content}`}</li>
      ))}
    </ul>
    <div>
      <button
        disabled={page === 0}
        onClick={() => setPage((old) => Math.max(old - 1, 0))}
      >
        &lt;
      </button>
      <button
        disabled={isPlaceholderData || !postsData?.hasMore}
        onClick={() => setPage((old) => old + 1)}
      >
        &gt;
      </button>
    </div>
  </div>
  
);

이제 pagination 구현이 성공적으로 완료되었다.

여기서 딱 한 가지만 더 보완을 해보자면, 데이터를 prefetch하여 다음 페이지로의 전환을 끊김없이 만들어보는 것이다.

이는 쿼리 클라이언트의 prefetchQuery 함수를 이용하여 구현할 수 있다.

...

useEffect(() => {
  if (!isPlaceholderData && postsData?.hasMore) {
    queryClient.prefetchQuery({
      queryKey: ['posts', page + 1],
      queryFn: () => getPosts(page + 1, PAGE_LIMIT),
    });
  }
}, [isPlaceholderData, postsData, queryClient, page]);
...
profile
프론트엔드 개발자가 되기 위해 노력 중인 새싹🌱 입니다.

0개의 댓글