프로젝트를 진행 하다 보면 리스트 페이지에서 pagination기능을 적용하고는 한다. 따로 라이브러리는 사용하지 않고 자바스크립트로 pagination을 구현했다.
현재 제작한 개인 블로그에서도 한 페이지 당 9개 포스트로 조건 별로 pagination을 구현했다.
기본 pagination
Pagination 설정
- currentPage : 초기 값 1로 설정하여 첫 페이지부터 보이게 한다.
- indexOfLast / indexOfFirst ****: 현재 페이지에 따라 보여줄 블로그 목록의 시작과 끝 인덱스를 계산한다.
- {blogs?.results.slice(indexOfFirst, indexOfLast).map((item: ListResults) => ...} ****: 현재 페이지에 해당하는 블로그 목록을 슬라이스 해 블로그 아이템에 대해 Post 컴포넌트를 렌더링 한다.
- <Pagination ... /> : 페이지네이션 컴포넌트를 렌더링하고 필요한 props를 전달한다.
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 컴포넌트
- totalPages : 전체 페이지 수를 계산한다. ( 전체 포스트의 수를 한 페이지에 표시되는 포스트의 수로 나누어 총 페이지 수를 계산함. Math.ceil() 함수는 주어진 숫자를 올림하여 가장 가까운 정수로 반환 )
- displayPages : 현재 화면에 보여질 페이지 번호들을 저장할 배열
- sidePageNumbers : 현재 페이지 주변에 표시할 페이지 번호의 개수를 결정한다.
( ex) maxPageNumbers가 5라면, sidePageNumbers는 2가 됩니다. 이는 현재 페이지를 중심으로 좌우로 2개의 페이지 번호를 표시하도록 하는 것입니다. sidePageNumbers 을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위를 동적으로 계산하여 페이지네이션 컴포넌트가 적절한 수의 페이지 번호를 보여준다. Math.floor() 함수는 소수점 이하를 버린다. )
- startPageNumber / endPageNumber: 화면에 보여질 페이지 번호의 시작과 끝을 계산한다.
if문을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위가 전체 페이지 수를 초과하지 않도록 조건을 걸었다.
- <BsChevronLeft /> / <BsChevronRight /> : 클릭 시 현재 페이지에서 전, 후 페이지로 이동한다.
- <BsChevronDoubleLeft /> / <BsChevronDoubleRight /> : 클릭 시 현재 페이지에서 10페이지 전, 10페이지 후로 이동한다.
// 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 기능을 추가하면서 문제가 발생했다.
- 태그 선택 시 포스트 수 변화
- 태그 선택 시 첫 페이지부터 시작 필요 ( ex). emotion 태그에서 3번 페이지에 있다가 css 페이지 누르면 css의 1번 페이지가 보여야 한다.
- 정렬 순서 변경 시 페이지 마다가 아닌 전체 데이터 순서 변경
해결 방법
- 태그 선택 시 포스트 수 변화
검색 기능에서 사용했던 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>
</>
);
}
결과
'React' 카테고리의 다른 글
React Query(Tanstack Query) (0) | 2024.08.30 |
---|---|
눈 내리는 효과 (0) | 2024.08.28 |
검색 기능 (0) | 2024.08.26 |
react-joyride (0) | 2024.08.26 |
React 에서 탭 기능 구현 (0) | 2024.08.26 |