[React Query] 초간단 페이지네이션 구현하기
![[React Query] 초간단 페이지네이션 구현하기](https://cdn.hashnode.com/res/hashnode/image/upload/v1723265570792/e3ae4a15-bd50-4340-b742-e92650dc40e7.png)
제작년에 자바스크립트로 페이지네이션을 한땀한땀 구현하다가 너무 구려서 때려친 기억이 있다. 그 이후론 페이지네이션을 만들 일이 없어서 잊고 살았는데...
회사에서 다음주부터 내가 담당할 기능에 대해 간단히 설명해주셨는데 페이지네이션 기능이 있는 것이다! 내 머리를 쥐나게 했던 그 놈!🤯
이 프로젝트에선 React와 React Query를 사용하기 때문에 나도 이걸 사용해서 미리 만들어보는 예습을 하고 그 과정을 간단히 정리해보고자 한다.
1. 기획 및 API 살펴보기
내가 페이지네이션에서 구현하고 싶은 기능은 다음과 같다:
검색창에 도서명 검색
검색 결과를 10개씩 보여주기
Prev/Next 버튼을 통해 데이터 추가적으로 가져오기
페이지 번호 버튼을 통해 사용성 높이기
첫 번째/마지막 페이지에서 Prev/Next 버튼 비활성화
로딩, 에러 처리
알라딘에서 제공하는 도서 검색 API를 사용할 것이다. 알라딘 API 사용법은 다음을 참고한다.
2. React Query 초기 세팅
npm i @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function Providers({ children }: ProviderProps) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
설치 과정은 React Query 공식문서에서 자세히 볼 수 있다.
3. 데이터 패칭
const SearchResult = ({ searchParam }: SearchResultProps) => {
const LIMIT = 10
const [page, setPage] = useState(1)
const [totalResults, setTotalResults] = useState(0)
const getSearchBooks = async ({ searchParam, page }: { searchParam: string; page: number }) => {
try {
const response = await fetch(`/api/book/search?search=${searchParam}&startIndex=${page}`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
setTotalResults(data.totalResults)
return data
} catch (error) {
console.error(error)
throw error
}
}
const { data } = useQuery({
queryKey: ['books', searchParam, page],
queryFn: () => getSearchBooks({ searchParam, page }),
})
return (
// 도서 목록 결과
<ul>
{data?.item?.map(({ isbn, title }: { isbn: string; title: string }, index: number) => (
<li key={isbn}>
{LIMIT * (page - 1) + index + 1}. {title}
</li>
))}
</ul>
)
}

LIMIT: 데이터를 한 번 패치할 때 가져올 개수. 알라딘 API는 기본적으로 10개로 정의한다.page: 현재 위치한 페이지. 초기값을 1로 설정하고 버튼을 통해 페이지 숫자를 변경한다.totalResults: 전체 데이터 개수. 페이지 버튼을 사용하기 위해 반드시 필요하다. API에서 제공하는지 반드시 확인해야 한다.
getSearchBooks 함수를 통해 비동기로 데이터를 패치하도록 한다. 검색어인 searchParam과 불러올 index인 page를 매개변수로 정의한다.
useQuery를 사용하여 getSearchBooks 함수를 호출하여 데이터를 패치한다. queryKey의 index 0번은 유니크한 값을, index 1부터는 의존성을 나타낸다. 의존성에 해당하는 값이 바뀔 때마다 패치한다.
4. Prev/Next 버튼 기능
const SearchResult = ({ searchParam }: SearchResultProps) => {
...
const totalPage = useMemo(() => Math.ceil(totalResults / LIMIT), [totalResults])
const hasNext = useMemo(() => page < totalPage, [page, totalPage])
return (
<>
{/* 도서 목록 결과 */}
<button onClick={() => setPage((old) => old - 1)} disabled={page === 1}>
Prev
</button>
<button
onClick={() => hasNext && setPage((old) => old + 1)}
disabled={!hasNext}
>
Next
</button>
</>
)
}


totalPage: 전체 페이지 개수hasNext: 현재 페이지가 마지막 페이지보다 작으면 다음 페이지에 데이터가 존재함
Prev 버튼을 클릭하면 이전 페이지로 상태가 변경되고 새로운 데이터를 패치한다. 현재 페이지가 1인 경우 이전 데이터가 존재하지 않으므로 비활성화한다.
Next 버튼을 클릭하면 다음 페이지가 존재하는지 확인하고 다음 페이지로 상태가 변경되고 새로운 데이터를 패치한다. 다음 페이지가 존재하지 않으면 비활성화한다.
5. 페이지 버튼 기능
const SearchResult = ({ searchParam }: SearchResultProps) => {
const MAX_BUTTON = 5
...
const startPage = useMemo(() => Math.max(page - Math.floor(MAX_BUTTON / 2), 1), [page])
const endPage = useMemo(() => Math.min(startPage + MAX_BUTTON - 1, totalPage), [startPage, totalPage])
return (
<>
{/* 도서 목록 결과 */}
{/* Prev 버튼 */}
{Array.from({ length: endPage - startPage + 1 }, (_, index) => {
const currentPage = startPage + index
return (
<button key={currentPage} onClick={() => setPage(currentPage)} disabled={currentPage === page}>
{currentPage}
</button>
)
})}
{/* Next 버튼 */}
</>
)
}


MAX_BUTTON: 한 번에 표시할 최대 페이지 버튼 수startPage,endPage: 현재 페이지를 중심으로MAX_BUTTON개 보여주는 로직, 현재 페이지 숫자에 따라 바뀐다
화면에 표시될 페이지 버튼 수만큼 순환한다. startPage에 index를 더하면 해당 버튼의 페이지 수가 된다.
6. 그 외 자잘한 기능
const SearchResult = ({ searchParam }: SearchResultProps) => {
...
const { isPending, isError, error, data, isFetching } = useQuery({
queryKey: ['books', searchParam, page],
queryFn: () => getSearchBook({ searchParam, page }),
placeholderData: keepPreviousData,
})
...
return (
<>
{isPending ? (
<div>Pending...</div>
) : isError ? (
<div>Error: {error.message}</div>
) : (
{/* 도서 목록 결과 */}
)}
{/* 페이지네이션 버튼 */}
</>
)
}
isPending: 데이터가 아직 존재하지 않는 경우isError: 데이터를 가져오는데 실패한 경우keepPreviousData: 현재 페이지에서 다른 페이지로 넘어갈 때 보이는Pending...요소를 숨기고 다른 페이지로 넘어갈 때까지 현재 페이지를 유지시킨다.

이렇게 페이지네이션 기능을 완성하였다🎉
![[JavaScript] 이미지 사이즈 압축해서 용량 줄이기](https://cdn.hashnode.com/res/hashnode/image/upload/v1731718582763/ed9e5687-ee04-44d6-ae39-8e5a809bdf73.png)
![[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기](https://cdn.hashnode.com/res/hashnode/image/upload/v1727921664973/6e3f32b0-2df4-44f9-bc12-69589dffbcb7.png)
![[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)](https://cdn.hashnode.com/res/hashnode/image/upload/v1725535323097/421ac5cd-a7af-401a-80f3-24b15fcd6087.png)