
나는 이전 프로젝트는 물론 현재 프로젝트까지 react-paginate 라이브러리를 활용해서 페이지네이션을 구현했었다.
하지만 이번 기회에 라이브러리없이 리팩토링을 결심했다.
결심한 이유는 크게 두가지이다.
결과적으로 에러없이 잘 구현했지만 문득 페이지네이션의 내부 구현방식이 궁금했다.
개발 속도를 높이기위해 라이브러리에 의존했던 나를 뒤돌아보며 기본기를 탄탄하기 하기 위해 라이브러리를 사용하지않고 리팩토링을 결심했다.
일단 라이브러리로 만든 코드를 뜯어냈다.
function Pagination({ onChangePage, totalPages, page }: PaginationProps) {
return (
<div>
<ReactPaginate
className={`${styles.pagination} ${
page === 1 ? styles.Pagination_pagination__cjT7I : undefined
} `}
pageCount={totalPages}
pageRangeDisplayed={5}
marginPagesDisplayed={5}
onPageChange={({ selected }) => onChangePage(selected + 1)}
previousLabel={<FaArrowLeft />}
nextLabel={<FaArrowRight />}
activeClassName={page === 1 ? undefined : styles.activePage}
/>
</div>
);
}
export default Pagination;
리팩토링 전 코드이다.
일단 ReactPaginate 컴포넌트를 주석처리하고 내가 페이지네이션을 구현하기위해 필요한 정보가 무엇이 있을까 생각해보았다.
이 세개가 필요했다.
마지막 페이지인건 어떻게 내가 알 수 있을까?
마지막페이지 번호가 꼭 5개가 된다는 보장이 안되는데 (밑에 번호가 5개씩 나오도록 설계했다) 계속 5개씩 보여준다고 짜면 오류가 나지 않을까?
const [currentPage, setCurrentPage] = useState<number[]>([]);
const initPageNumber = useCallback((totalPages: number, page: number) => {
const viewPages = [];
for (let i = page; i <= Math.min(page - 1 + LIMIT, totalPages); i += 1) {
viewPages.push(i);
}
setCurrentPage(viewPages);
}, []);
Math.min(page - 1 + LIMIT, totalPages)
이 부분에서 마지막 구간에 페이지가 몇개 있는지 판단했다.
만약 마지막페이지가 2개밖에 보여줄게없다면 totalPage가 page -1 + LIMIT보다 작아서 번호가 2개만 나올것이다.
또한 마지막페이지는 totalPages를 가지고있어서 같이 잘 해결되었다.
import React, { useCallback, useEffect, useState } from 'react';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import styles from './index.module.scss';
type PaginationProps = {
onChangePage: (page: number) => void;
totalPages: number;
page: number;
};
const LIMIT = 5;
function Pagination({ onChangePage, totalPages, page }: PaginationProps) {
const [currentPage, setCurrentPage] = useState<number[]>([]);
const initPageNumber = useCallback((totalPages: number, page: number) => {
const viewPages = [];
for (let i = page; i <= Math.min(page - 1 + LIMIT, totalPages); i += 1) {
viewPages.push(i);
}
setCurrentPage(viewPages);
}, []);
const prePages = () => {
onChangePage(currentPage[0] - 5);
initPageNumber(totalPages, currentPage[0] - 5);
};
const nextPages = () => {
onChangePage(currentPage[0] + 5);
initPageNumber(totalPages, currentPage[0] + 5);
};
useEffect(() => {
initPageNumber(totalPages, page);
}, []);
return (
<div className={styles.pagination}>
<button disabled={currentPage[0] === 1} onClick={prePages} type="button">
<FaArrowLeft color="#f57a00" />
</button>
<ul>
{currentPage?.map((v) => {
return (
<li
className={v === page ? styles.activePage : undefined}
onClick={() => onChangePage(v)}
key={v}
>
{v}
</li>
);
})}
</ul>
<button
disabled={currentPage[currentPage.length - 1] === totalPages}
onClick={nextPages}
type="button"
>
<FaArrowRight color="#f57a00" />
</button>
</div>
);
}
export default Pagination;

페이지를 이동할 때 사용자 입장에서 첫번째 페이지를 눌렀을 때
그 다음 가장 누를 확률이 가장 큰 버튼인 두번째 페이지라고 생각했다.
나는 사용자가 페이지 번호를 클릭하면 클릭한 다음 페이지 게시글 데이터들을 미리 가져와 캐싱하면 사용자가 좀 더 빠르게 페이지를 볼 수 있지않을까? 라는 생각을 했고 구글링 끝에 react-query의 queryClient.prefetchQuery라는 문법을 사용해 성능을 향상 시켰다.
pre+fetching == 즉 미리 fetching을 하겠다는 뜻이다!
next 버튼을 누르면 currentPage가 바뀌면서 useEffect함수가 실행된다.
useEffect함수는 페이지가 바뀔떄마다 그 페이지의 다음페이지를 미리 fetching해주는 함수다.
fetching을 한다면 데이터가 캐시에 저장이 되기떄문에 로딩없이 바로 임시로 데이터를 보여줄 수 있다.
결론적으로 사용자에게 편리한 ux를 보여줄 수 있다.
페이지가 바뀔떄마다 useEffect를 호출하게 한건 페이지의 수를 바꿔주는 함수가 비동기이기때문에 useEffect로 순서를 맞춰준것이다.
useEffect(() => {
if (currentPage < maxPostPage) {
let nextPage = currentPage + 1;
queryClient.prefetchQuery(["posts", nextPage], () => {
fetchPosts(nextPage);
}); // 미리 캐시에 담아두기 캐시에 데이터가 있어서 임시로 데이터를 보여줄수있음
}
}, [currentPage, useQueryClient]);
함수안에 queryClient.prefetchQuery는 pre+fetch의 합성어이다.
미리 fetching을 시도 하겠다라는 뜻이다!
maxPostPage가 10일떄 currentPage가 10이라면 호출이 안되게 막아놨다.
왜냐하면 currentPage가 10 이라면 11의 데이터가 호출되어야하는데 11 데이터는 없기떄문이다.
'use client';
import React, { useEffect, useState } from 'react';
import useInput from '@/src/hooks/useInput';
import { SelectValueType } from '@/src/types/search';
import usePage from '@/src/hooks/usePage';
import {
keepPreviousData,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { GetBoards } from '@/src/apis/Auction';
import FilterBox from '../FilterBox';
import styles from './index.module.scss';
import UserAuctionData from '../UserAuctionData';
const UserAuction = () => {
const queryClient = useQueryClient();
const [searchData, onchange] = useInput();
const { page, onChangePage } = usePage();
const [selectValue, setSelectValue] = useState<SelectValueType>({
area: '',
city: '',
category: '',
});
const {
isPending,
data: Boards,
refetch,
} = useQuery({
queryKey: ['Boards', page] as const,
queryFn: ({ queryKey }) => {
const page = queryKey[1];
const data = { ...selectValue, searchData, page };
return GetBoards(data);
},
placeholderData: keepPreviousData,
});
const fetchBoardList = (page: number) => {
const data = { ...selectValue, searchData, page };
return GetBoards(data);
};
const prefetchNextPosts = (page: number) => {
queryClient.prefetchQuery({
queryKey: ['Boards', page + 1],
queryFn: () => fetchBoardList(page + 1),
});
};
useEffect(() => {
if (page < Boards?.totalPages) {
prefetchNextPosts(page);
}
}, [page, prefetchNextPosts, useQueryClient]);
return (
<main className={styles.auction}>
<FilterBox
refetch={refetch}
searchData={searchData}
onchange={onchange}
selectValue={selectValue}
setSelectValue={setSelectValue}
onChangePage={onChangePage}
/>
<UserAuctionData
page={page}
Boards={Boards}
isPending={isPending}
onChangePage={onChangePage}
/>
</main>
);
};
export default UserAuction;

라이브러리없이 페이지네이션을 구현했다.
비록 라이브러리만큼 다양한 기능을 100% 똑같이 구현하진 못했지만 한번이라도 이렇게 자바스크립트를 써보면서 경험해보는것도 좋은 경험이라고 생각한다.
더해서 리액트쿼리를 사용해 성능 최적화까지 눈으로 확인할 수 있었다.
단순한 상황에서도 성능 최적화는 필요하다고 생각이 들었고 그만큼 신경쓰고 공부를 해야겠다는 생각이 들었다.
안녕하세요! 유익한 글 잘 봤습니다~ 디자인도 너무 귀여워서 css는 어떤 식으로 작성했는지 궁금해서요! 혹시 알 수 있을까요~?