프로젝트를 진행 하다 보면 리스트 페이지에서 pagination기능을 적용하고는 한다. 따로 라이브러리는 사용하지 않고 자바스크립트로 pagination을 구현했다.
현재 제작한 개인 블로그에서도 한 페이지 당 9개 포스트로 조건 별로 pagination을 구현했다.
기본 pagination
Pagination 설정
props
postsPerPage : 각 페이지에 표시되는 블로그 포스트의 수 ( 예시에서는 한 페이지당 9개의 포스트가 보여준다. )
totalPosts : 전체 블로그 포스트의 수
currentPage : 현재 화면에 표시되는 페이지의 번호
setCurrentPage : 페이지를 변경할 때 호출되는 콜백 함수로, 현재 페이지를 업데이트한다.
import Pagination from "@/components/ScreenElement/pagination"; export default function Blog({ blogs }: BlogistObject) { // Paging const [currentPage, setCurrentPage] = useState(1); const indexOfLast = currentPage * 9; const indexOfFirst = indexOfLast - 9; return ( <> . ... <div> {blogs?.results.slice(indexOfFirst, indexOfLast).map((item: ListResults) => ( <Post key={item.id} item={item} viewStyle={"gallery"} tagCategory={"All"} /> ))} </div> <Pagination postsPerPage={9} totalPosts={blogs?.results.length || 0} currentPage={currentPage} setCurrentPage={setCurrentPage} /> </> ); }
Pagination 컴포넌트
( ex) maxPageNumbers가 5라면, sidePageNumbers는 2가 됩니다. 이는 현재 페이지를 중심으로 좌우로 2개의 페이지 번호를 표시하도록 하는 것입니다. sidePageNumbers 을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위를 동적으로 계산하여 페이지네이션 컴포넌트가 적절한 수의 페이지 번호를 보여준다. Math.floor() 함수는 소수점 이하를 버린다. )
if문을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위가 전체 페이지 수를 초과하지 않도록 조건을 걸었다.
// pagination.tsx import { PaginationType } from "@/InterfaceGather"; import { cls } from "libs/utils"; import { BsChevronDoubleLeft, BsChevronDoubleRight, BsChevronLeft, BsChevronRight } from "react-icons/bs"; export default function Pagination({ postsPerPage, totalPosts, currentPage, setCurrentPage, }: PaginationType) { const totalPages = Math.ceil(totalPosts / postsPerPage); const displayPages = []; const maxPageNumbers = 5; const sidePageNumbers = Math.floor(maxPageNumbers / 2); let startPageNumber = currentPage - sidePageNumbers; let endPageNumber = currentPage + sidePageNumbers; if (startPageNumber <= 0) { startPageNumber = 1; endPageNumber = Math.min(totalPages, maxPageNumbers); // startPageNumber를 1로 설정하여 음수나 0을 피하고, // endPageNumber를 totalPages와 maxPageNumbers 중 작은 값으로 설정하여 // 페이지 범위가 최대 페이지 수를 초과하지 않도록 한다. } if (endPageNumber > totalPages) { startPageNumber -= endPageNumber - totalPages; endPageNumber = totalPages; // startPageNumber를 현재 값에서 초과한 만큼 감소시켜 // 페이지 번호가 어긋나지 않도록 조절하고, endPageNumber를 // totalPages로 설정하여 페이지 범위가 최대 페이지 수를 초과하지 않도록 합니다. } for (let i = startPageNumber; i <= endPageNumber; i++) { displayPages.push(i); } const showNum = displayPages.filter((n) => { return n > 0; }); return ( <ul className="pagination-style"> {currentPage > 10 && ( <li className="arrow aLeftDouble" onClick={() => setCurrentPage(currentPage - 10)} > <BsChevronDoubleLeft /> </li> )} {currentPage > 1 && ( <li className="arrow aLeft" onClick={() => setCurrentPage(currentPage - 1)} > <BsChevronLeft /> </li> )} {startPageNumber > 1 && ( <li onClick={() => setCurrentPage(1)}> <span>1</span> </li> )} {startPageNumber > 2 && ( <li className="ellipsis"> <span>...</span> </li> )} {showNum.map((number: any) => ( <li key={number} onClick={() => setCurrentPage(number)} className={cls(currentPage === number ? "currentpage" : "")} > {number} </li> ))} {endPageNumber < totalPages - 1 && ( <li className="ellipsis"> <span>...</span> </li> )} {endPageNumber < totalPages && ( <li onClick={() => setCurrentPage(totalPages)}> <span>{totalPages}</span> </li> )} {currentPage < totalPages && ( <li className="arrow aRight" onClick={() => setCurrentPage(currentPage + 1)} > <BsChevronRight /> </li> )} {currentPage <= totalPages - 10 && ( <li className="arrow aRightDouble" onClick={() => setCurrentPage(currentPage + 10)} > <BsChevronDoubleRight /> </li> )} </ul> ); }
Pagination 타입
// Interface.ts export interface PaginationType { postsPerPage: number; totalPosts: number; currentPage: number; setCurrentPage: React.Dispatch<React.SetStateAction<number>>; }
블로그 적용 사항
기존에 tagCategory 중 하나를 누르면 해당 tag가 포함된 포스트만 필터 되어져 화면에 보여줬다. pagination 기능을 추가하면서 문제가 발생했다.
해결 방법
검색 기능에서 사용했던 filteredList 를 활용하여 조건에 해당하는 포스트 만 필터 된 데이터를 filteredList 에 저장하여 useEffect를 활용해 tagCategory 변경 할 때마다 tagCategoryCount() 를 호출한다. filteredList 에 있는 블로그 목록을 블로그 아이템에 대해 Post 컴포넌트를 렌더링 한다.
useEffect를 활용해 tagCategory 변경 할 때마다 setCurrentPage(1) 를 주어 현재 페이지 첫 번째 페이지로 업데이트한다.
블로그에서 포스트 글을 정렬 변경 시 순서가 바꿀 수 있다. 기본에서 처럼 데이터를 미리 slice() 하면 각 페이지내에서만 정렬이 변경된다. 그래서 정렬 함수로 만든 useSortedData() 안에서 Post 컴포넌트를 렌더링하고 렌더링된 데이터를 .slice(indexOfFirst, indexOfLast) 하면 전체 데이터 순서가 변경된다.
const tagCategoryCount = () => { let tagCount = blogs.results; if (tagCategory !== DEFINE.TAGCATEGORY.ALL) { tagCount = blogs.results.filter((item: ListResults) => { return item?.properties["태그"].multi_select .map((row: any) => row.name) .includes(tagCategory); }); } if (tagCategory === DEFINE.TAGCATEGORY.ETC) { tagCount = blogs.results.filter((item: ListResults) => { return item?.properties["태그"].multi_select .map((row: any) => row.name) .some((i) => [DEFINE.TAGCATEGORY.HTML, DEFINE.TAGCATEGORY.NEXTJS].includes(i) ); }); } setFilteredList(tagCount); }; useEffect(() => { tagCategoryCount(); setCurrentPage(1); }, [tagCategory]);
전체 코드
/* eslint-disable react-hooks/rules-of-hooks */ import Post from "@/components/post"; import { BASE_URL, DATABASE_ID_BLOG, TOKEN } from "libs/config"; import { useSortedData } from "libs/usePageState"; import { useBlogPageStore } from "@/store/pageStore"; import { useEffect, useState } from "react"; import DEFINE from "@/constant/Global"; import { BlogistObject, ListResults } from "@/InterfaceGather"; import Pagination from "@/components/ScreenElement/pagination"; export default function Blog({ blogs }: BlogistObject) { const [tagCategory, setTagCategory] = useState<string>(DEFINE.TAGCATEGORY.ALL); const [filteredList, setFilteredList] = useState<ListResults[]>([]); useEffect(() => { setFilteredList(blogs.results); }, [blogs.results]); // Paging const [currentPage, setCurrentPage] = useState(1); const indexOfLast = currentPage * 9; const indexOfFirst = indexOfLast - 9; const tagCategoryCount = () => { let tagCount = blogs.results; if (tagCategory !== DEFINE.TAGCATEGORY.ALL) { tagCount = blogs.results.filter((item: ListResults) => { return item?.properties["태그"].multi_select .map((row: any) => row.name) .includes(tagCategory); }); } if (tagCategory === DEFINE.TAGCATEGORY.ETC) { tagCount = blogs.results.filter((item: ListResults) => { return item?.properties["태그"].multi_select .map((row: any) => row.name) .some((i) => [DEFINE.TAGCATEGORY.HTML, DEFINE.TAGCATEGORY.NEXTJS].includes(i) ); }); } setFilteredList(tagCount); }; useEffect(() => { tagCategoryCount(); setCurrentPage(1); }, [tagCategory]); return ( <> <div className="laptop-max-width"> .... <div className="post-content-area"> ... <div className="page-state-style"> <ul className="item-tagCategory"> <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.ALL)}> {DEFINE.TAGCATEGORY.ALL}({blogs.results.length}) </li> <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.DEV)}> {DEFINE.TAGCATEGORY.DEV} </li> ..... // 태그 li <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.ETC)}> {DEFINE.TAGCATEGORY.ETC} </li> </ul> <PageState path={"blogs"} /> </div> <div> {useSortedData( filteredList.map((item: ListResults) => ( <Post key={item.id} item={item} viewStyle={viewStyle} tagCategory={tagCategory} /> )), sortedContent ).slice(indexOfFirst, indexOfLast)} </div> ... </div> <Pagination postsPerPage={9} totalPosts={filteredList?.length || 0} currentPage={currentPage} setCurrentPage={setCurrentPage} /> </div> </> ); }
결과