[TanstackQuery] 페이지네이션

강연주·2025년 1월 22일

 🏝️ TanstackQuery

목록 보기
7/7

Pagination 구현

데이터를 세 개씩 끊어서 불러오고, 다음 페이지 버튼을 누르면 그 다음 데이터를 세 개씩 받아오는 동작.

데이터 받아오는 api 함수가 page와 limit 값을 받도록 한다.
(백엔드는 쿼리 파라미터로 page와 limit를 넘겨주면 해당 pagedml 데이터를 limit 개수 만큼 보내주도록 설계되어 있음)

해당 page 데이터 페칭

🖥️ api.js

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에서 다음과 같이 queryKey에 page를 추가해, page별로 데이터를 캐싱하도록 한 후, 쿼리 함수 호출 부분에 page를 추가한다.

🖥️ HomePage.js

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

const PAGE_LIMIT = 3;

function HomePage() {
  const [page, setPage] = useState(0);
  const {
    data: postData,
    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} onChage={handleInputChange} />
          <button disabled={!content}>업로드</button>
        </form>
      </div>

      <div>
        <ul>
          {posts.map(post => {
            <li key={post.id}>
              {post.user.name}: {post.content}
            </li>;
          })}
        </ul>
      </div>
    </>
  );
}

첫 페이지(page = 0)에 해당하는 데이터만 보이게 된다.


페이지 변경 버튼 추가

탠스택 쿼리 개발자 도구로 posts 데이터를 보면, hasMore값이 존재한다. 백엔드에서 그 다음 페이지가 있을 때 hasMore = true로 보내준다. 이 값에 따라 다음 페이지 버튼을 비활성화할지 말지 결정한다.

🖥️ Homepage.js
...

  return (
    <>
      <div>
        {currentUserName ? loginMessage : <button onClick={handleLoginButtonClick}>codeit으로 로그인</button>}
        <form onSubmit={handleSubmit}>
          <textarea name="content" value={content} onChage={handleInputChange} />
          <button disabled={!content}>업로드</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}
          onClick={() => setPage((old) => old + 1)}>&gt;</button>
        </div>
      </div>

    </>
  );
}

export default HomePage;

마지막 페이지로 가면 다음 페이지 버튼이 비활성화된다.


placeholderData로 부드러운 UI 전환 구현

현재는 다음 페이지로 넘어갈 때마다 매번 로딩메시지가 뜨는데, 이는 새로운 페이지에 해당하는 쿼리를 보낼 때마다 완전히 새로운 쿼리로 인식되고 계속 pending 상태가 되기 때문이다.

탠스택쿼리에서는 좀 더 부드러운 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,
});

다음 버튼을 누르면, fetching 상태지만 로딩 메시지가 뜨지 않고 이전 페이지 내용이 그대로 보인다. 데이터가 모두 받아와지면 다음페이지 내용으로 바뀐다.

이 과정에서 다음 페이지 버튼이 활성화되어 있는 것을 볼 수 있는데, 현재 보이는 데이터가 이전 데이터(placeholderData)라면 다음 페이지 버튼을 비활성화하도록 한다. 그렇지 않으면 유저가 다음 페이지 버튼을 여러 번 눌러 존재하지 않는 페이지로 리퀘스트가 갈 수도 있기 때문이다. useQuery의 리턴 값에서 isPlaceholderData 값을 활용한다.

🖥️ 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>
  
);

데이터 prefetch로 심리스 UI 만들기

❓Seamless UI 란?
➡️ Seamless의 뜻은 '원활한'인데, Seamless UX를 직역하면 '원활한 사용자 경험', 윤색하면 '매끄러운 사용자 경험' 정도로 말할 수 있다.

쿼리 클라이언트의 prefetchQuery 함수를 이용하면, 현재 내가 보고 있는 페이지가 1페이지여도, 백그라운드에서 2페이지 데이터를 미리 fetch해놓기 때문에, 다음 페이지 이동 시 전혀 어색함이나 끊김 없이 2페이지를 볼 수 있다.

🖥️ ...

useEffect(() => {
  if (!isPlaceholderData && postsData?.hasMore) {
    queryClient.prefetchQuery({
      queryKey: ['posts', page + 1],
      queryFn: () => getPosts(page + 1, PAGE_LIMIT),
    });
  }
}, [isPlaceholderData, postsData, queryClient, page]);
...

출처 : 코드잇 ReactQuery

profile
아무튼, 개발자

0개의 댓글