좀 더 어려운 페이지네이션 구현하기
https://xionwcfm.tistory.com/350
react-query에서 제공해주는 기본적인 페이지네이션 예제는 다음 링크에서 확인하실 수 있습니다.
이번에는 앞서 쌓은 선수지식을 기반으로
좀 더 그럴듯한 페이지네이션을 구현해봅시다.
앞선 포스트와 마찬가지로 코드 분리는 지양하면서
최대한 적은 파일만 보고도 구현가능하게 코드를 작성하겠습니다.
QueryClient 및 Provider , Suspense 설정에 관련된 부분은 위 링크를 참고해주세요
이미 알고있다면 자신이 편한 세팅으로 변경해주시면 되겠습니다.
완성된 화면을 보고 시작해봅시다.
prev button, next button은 다음 / 이전 페이지로 점프할 수 있으며
각 버튼을 클릭하면 해당하는 페이지로 이동합니다.
prefetch는 현재 페이지의 +1 페이지에 대해서만 미리 받아옵니다.
완성된 코드
AdvancedPagenation.tsx
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react';
interface AdvancedPagenationProps {}
interface PokeType {
name: string;
url: string;
}
interface APIType {
count: number;
next: string;
previous: string;
results: PokeType[];
}
interface PageOptionType {
listSize: number;
activePage: number;
buttonCount: number;
}
const getPagenation = async (queryKey: PageOptionType) => {
const offset = queryKey.activePage * queryKey.listSize;
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=20&offset=${offset}`,
);
return await response.json();
};
const AdvancedPagenation = ({}: AdvancedPagenationProps) => {
const queryClient = useQueryClient();
const [pageOption, setPageOption] = React.useState<PageOptionType>({
listSize: 20,
activePage: 0,
buttonCount: 10,
});
const firstNum =
pageOption.activePage - (pageOption.activePage % pageOption.buttonCount);
const pagenationQuery = useQuery<APIType>({
queryKey: ['pagenation', pageOption],
queryFn: () => getPagenation(pageOption),
keepPreviousData: true,
staleTime: 5000,
});
React.useEffect(() => {
const prefetchQuerykey = {
...pageOption,
activePage: pageOption.activePage + 1,
};
if (!pagenationQuery.isPreviousData) {
queryClient.prefetchQuery({
queryKey: ['pagenation', prefetchQuerykey],
queryFn: () => getPagenation(prefetchQuerykey),
});
}
}, [
pagenationQuery.data,
pagenationQuery.isPreviousData,
pageOption,
queryClient,
]);
return (
<div>
{pagenationQuery.data?.results.map((poke) => (
<div key={poke.name}>{poke.name}</div>
))}
<button
onClick={() =>
setPageOption((state) => ({
...state,
activePage: Math.max(firstNum - state.buttonCount, 0),
}))
}
>
Prev Button
</button>
{Array.from({ length: pageOption.buttonCount }).map((e, i) => (
<button
key={i}
onClick={() =>
setPageOption((state) => {
return { ...state, activePage: firstNum + i };
})
}
>
{firstNum + i + 1}
</button>
))}
<button
onClick={() =>
setPageOption((state) => ({
...state,
activePage: Math.min(
pagenationQuery.data?.count as number,
firstNum + pageOption.buttonCount,
),
}))
}
>
Next Button
</button>
</div>
);
};
export default AdvancedPagenation;
완성된 코드를 읽는것만으로도 이해되시는 분들이라면
여기서 포스트를 꺼주셔도 좋습니다.
사용하는 api는 전포스트와 마찬가지로 pokeapi입니다.
pokeapi에 대한 정보 역시 전포스트에서 확인할 수 있으므로
본 포스트에서는 굳이 서술하지 않겠습니다.
코드 자세히보기
interface PageOptionType {
listSize: number;
activePage: number;
buttonCount: number;
}
먼저 useState로 관리해주기 위해 타입을 정의했습니다.
listSize : 한번에 표시될 콘텐츠의 갯수
activePage: 현재 보여지고있는 페이지
buttonCount : 한번에 보여질 버튼의 갯수
이렇게 세가지의 값을 관리하는 객체를 만들것입니다.
const getPagenation = async (queryKey: PageOptionType) => {
const offset = queryKey.activePage * queryKey.listSize;
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=20&offset=${offset}`,
);
return await response.json();
};
앞서 만든 객체를 입력받아 pokeapi에 사용해줍니다.
const queryClient = useQueryClient();
const [pageOption, setPageOption] = React.useState<PageOptionType>({
listSize: 20,
activePage: 0,
buttonCount: 10,
});
이제 prefetch를 위한 usequeryClient와
페이지네이션을 관리할 useState를 만들어주겠습니다.
안에 넣어주는 값들은 원하는 초기값을 넣어주면 됩니다.
전 한번에 20개의 컨텐츠가 표시되면서
페이지는 1로 시작하고 버튼은 열개씩 표시되길 원하니
이렇게 표기해주었습니다.
const firstNum =
pageOption.activePage - (pageOption.activePage % pageOption.buttonCount);
firstNum은 버튼의 첫번째 값을 항상 일정하게 유지하기 위한 변수입니다.
const pagenationQuery = useQuery<APIType>({
queryKey: ['pagenation', pageOption],
queryFn: () => getPagenation(pageOption),
keepPreviousData: true,
staleTime: 5000,
});
useQuery를 작성해주겠습니다.
페이지네이션을 구현할때 중요한점은 keepPreviousData를 활성화시켜주는것이라는 점도
기억하면 좋습니다.
React.useEffect(() => {
const prefetchQuerykey = {
...pageOption,
activePage: pageOption.activePage + 1,
};
if (!pagenationQuery.isPreviousData) {
queryClient.prefetchQuery({
queryKey: ['pagenation', prefetchQuerykey],
queryFn: () => getPagenation(prefetchQuerykey),
});
}
}, [
pagenationQuery.data,
pagenationQuery.isPreviousData,
pageOption,
queryClient,
]);
다음은 prefetch를 위한 useEffect입니다.
만약 prefetch가 필요없는 옵션이라면 이부분은 생략해도 좋습니다.
jsx 로직 작성
{pagenationQuery.data?.results.map((poke) => (
<div key={poke.name}>{poke.name}</div>
))}
아이템들을 렌더링시켜주는 부분입니다.
<button
onClick={() =>
setPageOption((state) => ({
...state,
activePage: Math.max(firstNum - state.buttonCount, 0),
}))
}
>
Prev Button
</button>
이전으로 가는 버튼을 구현해줬습니다.
Math.max 메서드를 이용하여 0보다 낮은값으로 되돌아가려는 경우 0이 되도록 합니다.
<button
onClick={() =>
setPageOption((state) => ({
...state,
activePage: Math.min(
pagenationQuery.data?.count as number,
firstNum + pageOption.buttonCount,
),
}))
}
>
Next Button
</button>
다음으로 가는 버튼의 구현입니다.
{Array.from({ length: pageOption.buttonCount }).map((e, i) => (
<button
key={i}
onClick={() =>
setPageOption((state) => {
return { ...state, activePage: firstNum + i };
})
}
>
{firstNum + i + 1}
</button>
))}
1부터 10까지와 같이 넘버버튼들을 표시하는 로직입니다.
배열 정적메서드 Array.from() 에 유사배열객체를 넣어주면
유사배열객체가 가지고 있는 length 프로퍼티의 값만큼의 길이를 가진 배열이 생성되기때문에
이런 방식으로 map을 돌릴 수 있습니다.
마치며
아직 active 상태일때의 CSS 처리
데이터의 끝에 도달했을 때 렌더링되는 버튼을 제한하기
등의 기능은 구현하지 않은 상태이지만
대충 이런 형식으로 로직을 작성하면 돌아간다 정도로 봐주시면 감사하겠습니다.
'react' 카테고리의 다른 글
react-error-boundary 라이브러리로 에러처리하기 (0) | 2023.07.20 |
---|---|
react-query의 onError는 Deprecated 되었다. (0) | 2023.07.12 |
react-query를 이용해 pagenation을 구현해보자 (0) | 2023.06.29 |
forwardRef를 이용해 ref를 다는데... 타입은 어떻게 함? (1) | 2023.06.19 |
zod를 이용해 회원가입 폼을 만들어보기 (0) | 2023.06.18 |