pagenation이란?
페이지네이션은 일정한 갯수로 아이템을 보여주는 것을 의미할 수 있습니다.
페이지네이션을 위한 간단한 알고리즘이 존재하니 기억해두시면 좋습니다.
그렇게 어렵지는 않은데 버튼을 일정하게 유지시켜주기 위해서 필요한 알고리즘입니다.
예컨대 1부터 10까지의 페이지를 보여주는 위 그림과 같은 페이지버튼이 있다고 가정해보면
1 2 3 4 5 6 7 8 9 10 이 나열되어야 하고
11페이지로 넘어가게되면
11 12 13 14 15 16 17 18 19 20이 나열되어야 할 것입니다.
이를 위해서 사용되어야하는 알고리즘입니다.
const [listSize, setListSize] = React.useState(20);
const [activePage, setActivePage] = React.useState(1);
const [buttonCount, setButtonCount] = React.useState(10);
let firstNum = activePage - (activePage % listSize) + 1;
let lastNum = activePage - (activePage % listSize) + buttonCount;
React로 구현한다고하면 이와 같은 형태로 구현할 수 있을 것입니다.
listSize는 한번에 표시할 컨텐츠의 갯수를 의미합니다.
activePage는 현재 보고있는 페이지를 의미합니다.
buttonCount는 한번에 보여줄 버튼의 갯수를 의미합니다.
firstNum과 listNum은 버튼의 시작점과 끝점을 의미합니다.
하지만 이번에는 먼저 간단히 prev/next 버튼만 구현을 하고 넘어가보도록하겠습니다.
필요한 기능 정의하기
모두가 숫자 버튼이 있는 페이지네이션을 구현할 필요는 없습니다.
우선 기초적인 기능을 수행하는 페이지네이션을 만들어 볼 것입니다.
1. 20개 단위로 아이템을 불러올 수 있어야한다.
2. prev 버튼, next버튼을 누르면 페이지의 앞 / 뒤로 이동할 수 있어야 한다.
3. prefetch를 통해 다음에 볼 데이터를 미리 받아올 수 있어야한다.
이 세가지 조건을 만족하는 페이지네이션 예제를 만들어봅시다.
아주 운이 좋게도 리액트쿼리는 저 모든 요구사항을 만족시킬만큼 기능을 제공해줍니다.
공식문서의 예제를 참고하며 작성했습니다.
최대한 적은 파일로 예제를 확인할 수 있도록 하기 위해
모듈화를 지양하면서 작성했습니다.
사용하는 api는 pokemon api입니다.
환경 설정
vite / typescript로 구축한 프로젝트입니다.
npx create-vite@latest
https://github.com/XionWCFM/react-query-infinite
자세한 코드는 위 레포에서 확인할 수 있습니다.
npm i @tanstack/react-query @tanstack/react-query-devtools
아래 실습에서 사용할 react-query 버전은 v4.29.14입니다.
App.tsx
function App() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
return (
<QueryClientProvider client={queryClient}>
<main>
<React.Suspense fallback={<Loading />}>
<PagenationComponent />
</React.Suspense>
</main>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
);
}
export default App;
리액트 쿼리 사용을 위해서는 QueryClient와 QueryClientProvider가 필요합니다.
react가 제공하는 suspense 기능을 사용하고 싶으니
QueryClient의 인자를 전달하여 suspense:true를 주었습니다.
Suspense의 fallback으로 전달한 loading 컴포넌트는
쿼리를 받아오는 로딩 상태 동안 보여질 컴포넌트를 의미합니다.
PagenationComponent는 아직 만들지 않았으니 넘어갑시다.
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react';
interface PokeType {
name: string;
url: string;
}
interface APIType {
count: number;
next: string;
previous: string;
results: PokeType[];
}
interface PagenationComponentProps {}
const getPagenation = async (page: number = 0) => {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=20&offset=${page}`,
);
return await response.json();
};
const PagenationComponent = ({}: PagenationComponentProps) => {
const queryClient = useQueryClient();
const [page, setPage] = React.useState(20);
const pagenationQuery = useQuery<APIType>({
queryKey: ['pagenation', page],
queryFn: () => getPagenation(page),
keepPreviousData: true,
staleTime: 5000,
});
React.useEffect(() => {
if (!pagenationQuery.isPreviousData) {
queryClient.prefetchQuery({
queryKey: ['pagenation', page + 20],
queryFn: () => getPagenation(page + 20),
});
}
}, [pagenationQuery.data, pagenationQuery.isPreviousData, page, queryClient]);
console.log(pagenationQuery.data);
return (
<div>
{pagenationQuery.data?.results.map((poke) => (
<div key={poke.name}>{poke.name}</div>
))}
<button
onClick={() => {
setPage((state) => Math.max(state - 20, 0));
}}
>
prev button
</button>
<button
onClick={() => {
setPage((state) => state + 20);
}}
>
next button
</button>
</div>
);
};
export default PagenationComponent;
한파일에 모든것을 때려박은 형태로 작성해두었습니다.
하나하나 차근차근 분리해서 살펴보도록하겠습니다.
그 이전에 실제 동작이 어떻게 이루어지는지 확인하겠습니다.
네트워크창을 주목해서 보면 다음에 받아올 페이지를 미리 페칭해오는 것을 확인할 수 있습니다.
이제 하나하나 코드들을 살펴보도록 하겠습니다.
읽기만해도 코드가 이해되는 분들은 넘어가셔도 좋습니다.
const getPagenation = async (page: number = 0) => {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=20&offset=${page}`,
);
return await response.json();
};
useQuery의 queryFn에 들어갈 함수를 정의하는 부분입니다.
pokeapi는 limit과 offset을 설정해줄 수 있습니다.
20개씩 데이터를 받아올것이고 offset은 20씩 증가시키겠습니다.
만약 5, 10, 20 이런 형태로 선택할 수 있게 하고싶다면
limit에 들어갈 값도 동적으로 관리해주면 될것입니다.
다음은 받아오는 API의 타입을 정의해주겠습니다.
pokeapi의 data는 다음과 같은 형태로 보입니다.
count , next, previous, results 키가 존재하고
results 키는 name,url을 가진 객체들이 배열의 형태로 존재합니다.
interface PokeType {
name: string;
url: string;
}
interface APIType {
count: number;
next: string;
previous: string;
results: PokeType[];
}
따라서 간단하게 정의를 해주었습니다.
이제 리액트 컴포넌트 내부에서 useQuery로직을 작성하겠습니다.
const queryClient = useQueryClient();
const [page, setPage] = React.useState(20);
const pagenationQuery = useQuery<APIType>({
queryKey: ['pagenation', page],
queryFn: () => getPagenation(page),
keepPreviousData: true,
staleTime: 5000,
});
prefetch를 하기 위해 useQueryClient()를 사용하겠습니다.
또한 page값을 동적으로 관리해줄 것이니 useState로 상태를 만들어 관리해주겠습니다.
pagenationQuery는 useQuery의 반환값을 담을 것입니다.
리액트쿼리의 쿼리키는 마치 useEffect의 의존성 배열과도 비슷하게 생각할 수 있습니다.
keepPreviousData:true는 이전 데이터를 유지할 것인지를 묻는 옵션입니다.
staleTime은 데이터가 언제 stale해진 것으로 판단할지를 정하는 옵션입니다.
React.useEffect(() => {
if (!pagenationQuery.isPreviousData) {
queryClient.prefetchQuery({
queryKey: ['pagenation', page + 20],
queryFn: () => getPagenation(page + 20),
});
}
}, [pagenationQuery.data, pagenationQuery.isPreviousData, page, queryClient]);
이제 useEffect를 작성하고
if문에 해당되는 경우가 생기면 prefetchQuery를 날리는 로직을 작성하겠습니다.
isPreviousData는 boolean값이 담겨있는 프로퍼티입니다.
isPreviousData가 false일때 prefetching을 시도해주겠습니다.
이 useEffect가 있었기 때문에 앞서 본 시연화면에서
페이지가 넘어갈때마다 다음 페이지를 미리 prefetch할 수 있었던 것입니다.
<button
onClick={() => {
setPage((state) => Math.max(state - 20, 0));
}}
Math.max() 내장 메서드를 이용하여 state가 0미만으로 떨어지지 않게 관리해주었습니다.
마치며
이번 예제로 인해 어떤 느낌으로 페이지네이션을 구현할 수 있는지 알아보았으니
다음에는 좀 더 심화된 페이지네이션을 구현해보겠습니다.
'react' 카테고리의 다른 글
react-query의 onError는 Deprecated 되었다. (0) | 2023.07.12 |
---|---|
react-query를 이용해 pagenation을 구현해보자(2) (2) | 2023.06.30 |
forwardRef를 이용해 ref를 다는데... 타입은 어떻게 함? (1) | 2023.06.19 |
zod를 이용해 회원가입 폼을 만들어보기 (0) | 2023.06.18 |
zod 라이브러리 사용법 간단히 익히기 (0) | 2023.06.15 |