React로 Pagination 구현하기
프로젝트를 진행하면서 데이터가 많아지면 페이지네이션(Pagination)이 필요하게 된다.
라이브러리를 사용할 수도 있지만, 공부의 취지로 직접 구현해보기로 했다!
이번 포스팅에서는 React로 Pagination을 구현한 과정을 정리하려고 한다.
🌻 Pagination의 동작 구조
Pagination의 전반적인 동작 구조는 네이버 블로그의 Pagination을 참고했다.
1. 페이지 번호를 클릭하면 페이지가 변경된다. (URL의 쿼리 스트링 값이 업데이트된다.)
2. 현재 페이지에는 active 디자인을 적용한다.
3. 이전 및 다음 버튼은, 해당 방향으로 이동 가능한 페이지가 있을 때만 표시된다.
4. 이전 버튼을 클릭하면, 이전 페이지 그룹의 마지막 페이지로 이동한다.
5. 다음 버튼을 클릭하면, 다음 페이지 그룹의 첫 번째 페이지로 이동한다.
6. 각 페이지 번호 및 이전/다음 버튼은 Link 태그(a 태그)를 사용하여 구현되며,마우스를 올리면 클릭 시 이동할 URL이 화면 왼쪽 하단에 표시된다.
🌻 Pagination 컴포넌트 구현하기
Pagination 컴포넌트는 다음과 같은 Props를 받아서 동작한다.
- totalItems: 데이터의 총 개수
- itemCountPerPage: 페이지 당 보여줄 데이터 개수
- pageCount: 보여줄 페이지 개수
- currentPage: 현재 페이지 번호
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { styled } from "@stitches/react";
interface Props {
totalItems: number; // 전체 아이템 수
itemCountPerPage: number; // 페이지당 아이템 수
pageCount: number; // 한번에 보여줄 페이지 번호 수
currentPage: number; // 현재 페이지 번호
}
export default function Pagination({
totalItems,
itemCountPerPage,
pageCount,
currentPage,
}: Props) {
// 총 페이지 수 계산
const totalPages = Math.ceil(totalItems / itemCountPerPage);
// 현재 보여줄 페이지 그룹의 시작 번호를 관리하는 상태 변수
const [start, setStart] = useState(1);
// 현재 보여줄 페이지 그룹의 마지막 번호
const end = start + pageCount - 1;
// 이전 페이지로 이동할 수 있는지 확인
const noPrev = currentPage <= 1;
// 다음 페이지로 이동할 수 있는지 확인
const noNext = currentPage >= totalPages;
// 페이지가 바뀔 때마다 start 값을 조정하여 올바른 페이지 번호가 보이도록 함
useEffect(() => {
if (currentPage > end) {
setStart((prev) => prev + pageCount);
} else if (currentPage < start) {
setStart((prev) => Math.max(prev - pageCount, 1));
}
}, [currentPage, pageCount, start, end]);
return (
<PaginationWrapper>
<ul>
<li className={`move ${noPrev ? "invisible" : ""}`}>
<StyledLink to={`?page=${Math.max(currentPage - 1, 1)}`}>
{"<"}
</StyledLink>
</li>
{[...Array(Math.min(pageCount, totalPages - start + 1))].map((_, i) => (
<li
key={i}
className={`page ${currentPage === start + i ? "active" : ""}`}
>
{start + i <= totalPages && (
<StyledLink to={`?page=${start + i}`}>{start + i}</StyledLink>
)}
</li>
))}
<li className={`move ${noNext ? "invisible" : ""}`}>
<StyledLink to={`?page=${Math.min(currentPage + 1, totalPages)}`}>
{">"}
</StyledLink>
</li>
</ul>
</PaginationWrapper>
);
}
전체 코드는 위와 같고, 주요 내용을 상세히 살펴보자.
1. 총 페이지 수 계산
const totalPages = Math.ceil(totalItems / itemCountPerPage);
전체 데이터 개수를 페이지당 보여줄 데이터 개수로 나누어 총 페이지 수를 계산한다. 이 값은 페이지네이션이 몇 페이지까지 존재하는지를 결정하는 중요한 역할을 한다!
2. 상태 관리
const [start, setStart] = useState(1);
start는 현재 표시할 페이지 그룹의 시작 페이지 번호를 나타낸다.
예를 들어, 현재 페이지 그룹이 1~5라면, start는 1이다. 페이지가 이동할 때마다 이 값을 조정하여, 올바른 페이지 번호가 표시되도록 한다.
3. 이전과 다음 페이지 유무 확인
const noPrev = currentPage <= 1;
const noNext = currentPage >= totalPages;
noPrev와 noNext는 각각 이전 페이지 또는 다음 페이지로 이동할 수 있는지 여부를 확인한다.
이전 페이지가 없으면 noPrev는 true가 되고, 다음 페이지가 없으면 noNext가 true가 된다.
=> 이를 통해 "이전"과 "다음" 버튼의 활성화 여부를 결정한다
4. Pagination UI
return (
<PaginationWrapper>
<ul>
<li className={`move ${noPrev ? "invisible" : ""}`}>
<StyledLink to={`?page=${Math.max(currentPage - 1, 1)}`}>
{"<"}
</StyledLink>
</li>
{[...Array(Math.min(pageCount, totalPages - start + 1))].map((_, i) => (
<li
key={i}
className={`page ${currentPage === start + i ? "active" : ""}`}
>
{start + i <= totalPages && (
<StyledLink to={`?page=${start + i}`}>{start + i}</StyledLink>
)}
</li>
))}
<li className={`move ${noNext ? "invisible" : ""}`}>
<StyledLink to={`?page=${Math.min(currentPage + 1, totalPages)}`}>
{">"}
</StyledLink>
</li>
</ul>
</PaginationWrapper>
);
<ul> 태그 내에서 '페이지 번호'와 '이전/다음 버튼'을 렌더링한다.
- 현재 페이지는 active 클래스를 적용하여 강조된다.
- 이전/다음 버튼은 noPrev와 noNext에 따라 표시 여부가 결정된다.
- 사용자가 페이지 번호를 클릭하면 Link 태그가 사용되어 URL의 쿼리 스트링을 업데이트한다.
🌻Pagination 컴포넌트 스타일링
Pagination 컴포넌트의 UI는 stitches.js를 사용하여 스타일링했다
const PaginationWrapper = styled("div", {
display: "flex",
justifyContent: "center",
marginTop: "30px",
color: "#888",
fontSize: "14px",
ul: {
display: "flex",
listStyle: "none",
padding: "0",
},
li: {
float: "left",
height: "25px",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "0 5px",
borderRadius: "50%", // 원형으로 만들기 위해 추가
width: "25px", // 원형이 되도록 동일한 너비와 높이 설정
textAlign: "center",
"&.page": {
cursor: "pointer",
borderRadius: "50%", // 원형으로 만들기 위해 추가
border: "solid 1px rgba(0, 0, 0, 0)",
textAlign: "center",
"&:hover": {
border: "solid 1px #aaa",
},
},
"&.active": {
background: "#1C6BA4",
color: "white",
fontWeight: "bold",
borderRadius: "50%", // 원형 유지
width: "25px", // 원형이 유지되도록 동일한 너비와 높이 설정
height: "25px",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
},
".move": {
cursor: "pointer",
margin: "0 10px",
a: {
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: "10",
textAlign: "center",
fontSize: "20px", // 꺾쇠를 더 크게 보이도록 설정
"&:hover": {
textDecoration: "underline",
},
},
},
".invisible": {
visibility: "hidden",
},
});
const StyledLink = styled(Link, {
textDecoration: "none", // 기본 밑줄 제거
color: "inherit", // 부모 요소의 글자색을 따르도록 설정
"&.active": {
color: "white", // 활성화된 상태에서 흰색 글자
},
});
🌻Pagination 적용 예시
Pagination 컴포넌트를 실제 페이지에 적용하여 활동 목록을 페이징 처리할 수 있다.
전체 코드는 아래와 같다.
import { styled } from "@stitches/react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { FaSearch } from "react-icons/fa";
import ActivityUnitBox from "./ActivityUnitBox";
import DefaultImg from "../../assets/image/defalut.png";
import dayjs from "dayjs";
import Pagination from "../Pagination";
export default function ActivityContainer({ activities, selectedPeriod }) {
// Pagination 관련 코드
...
const [searchParams] = useSearchParams();
const currentPage = parseInt(searchParams.get("page") || "1", 10);
const itemsPerPage = 5; // 페이지당 표시할 활동 개수
const totalPages = Math.ceil(activitiesToDisplay.length / itemsPerPage);
return (
<ActivityContainerWrap>
...
// Pagination 관련 코드
<Pagination
totalItems={activitiesToDisplay.length}
itemCountPerPage={itemsPerPage}
pageCount={5}
currentPage={currentPage}
/>
</ActivityContainerWrap>
);
}
const ActivityContainerWrap = styled("div", {
display: "flex",
flexDirection: "column",
alignItems: "center",
});