프로젝트를 하면서 Pagination이 필요해 구현해 보기로 했다. 찾아보니 라이브러리도 여러 개 있던데 머리도 쓸 겸 그냥 직접 구현해 보기로 했다.
Pagination의 동작 구조
전반적인 동작 구조는 네이버 블로그에 있는 Pagination을 따라 했다.
1. 숫자를 누르면 페이지가 이동한다. (URL의 query string 값이 바뀐다.)
2. 현재 페이지에는 active 디자인을 적용해 준다.
3. 이전과 다음 글자는 이전 또는 다음 페이지가 존재할 경우에만 보이도록 한다.
4. 이전 버튼을 누르면 바로 이전의 end 지점(start 지점 + 보여줄 페이지수 - 1)로 이동한다.
5. 다음 버튼을 누르면 바로 다음의 start 지점으로 이동한다.
6. 각 숫자 및 글자는 Link 태그(a 태그)를 이용한다. > 마우스를 올렸을 때, 화면 왼쪽 하단에 클릭 시 이동할 경로가 보인다.
Pagination 구현하기
interface Props {
totalItems: number; // 데이터의 총 개수
itemCountPerPage: number; // 페이지 당 보여줄 데이터 개수
pageCount: number; // 보여줄 페이지 개수
currentPage: number; // 현재 페이지
}
Pagination 컴포넌트는 다음과 같은 Props 값을 받아오도록 한다.
나의 경우 데이터의 총개수는 백엔드에서 받아왔다.
const totalPages = Math.ceil(totalItems / itemCountPerPage); // 총 페이지 개수
const [start, setStart] = useState(1); // 시작 페이지
const noPrev = start === 1; // 이전 페이지가 없는 경우
const noNext = start + pageCount - 1 >= totalPages; // 다음 페이지가 없는 경우
필요한 state와 상수를 다음과 같이 정의했다.
- 전체 데이터 개수를 페이지당 보여줄 데이터 개수로 나누어 총 페이지 개수를 구한다.
- 이전 페이지와 다음 페이지가 있는 경우에만 이전/다음 문구를 보여줄 것이기 때문에 이를 판별할 상수를 각각 noPrev, noNext로 정의했다.
Pagination 그리기
<ul>
<li className={`${noPrev && styles.invisible}`}>
<Link to={`?page=${start - 1}`}>이전</Link>
</li>
{[...Array(pageCount)].map((a, i) => (
<>
{start + i <= totalPages && (
<li key={i}>
<Link className={`${currentPage === start + i && styles.active}`}
to={`?page=${start + i}`}>
{start + i}
</Link>
</li>
)}
</>
))}
<li className={`${noNext && styles.invisible}`}>
<Link to={`?page=${start + pageCount}`}>다음</Link>
</li>
</ul>
보여줄 페이지 개수만큼 배열을 생성하여 map 반복문으로 페이지 링크를 그려준다. start + i <= totalPages라는 조건문을 통해 전체 페이지 개수까지만 링크를 생성하도록 한다. 예를 들어 전체 페이지수가 18이고 보여줄 페이지 개수가 5개일 때, 해당 조건문이 없다면 16에서 18까지가 아닌 20까지 링크를 생성하게 된다.
이전을 누르면 바로 이전 end 지점으로 이동하도록 하고, 다음을 누르면 다음 start 지점으로 이동하도록 한다.
+) 나는 전체 페이지 개수가 20 이상으로 넘어갈 것 같지 않아서 이전/다음만 구현했지만 페이지수가 많은 경우, 맨 처음/맨 마지막도 구현하는 게 좋을 것 같다.
이 부분에서는 오직 페이지의 이동만을 담당하고 있다.
보여줄 페이지 설정
useEffect(() => {
if (currentPage === start + pageCount) setStart((prev) => prev + pageCount);
if (currentPage < start) setStart((prev) => prev - pageCount);
}, [currentPage, pageCount, start]);
useEffect를 통해 보여줄 페이지 숫자들을 설정했다. 현재 페이지를 기준으로 현재 페이지를 포함하는 범위의 숫자들을 보여준다. (ex. 현재 페이지 3일 때: 1~5 출력, 현재 페이지 8일 때: 6~10 출력)
전체 코드
// pagination.tsx
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import styles from "../styles/scss/pagination.module.scss";
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 noPrev = start === 1;
const noNext = start + pageCount - 1 >= totalPages;
useEffect(() => {
if (currentPage === start + pageCount) setStart((prev) => prev + pageCount);
if (currentPage < start) setStart((prev) => prev - pageCount);
}, [currentPage, pageCount, start]);
return (
<div className={styles.wrapper}>
<ul>
<li className={`${styles.move} ${noPrev && styles.invisible}`}>
<Link to={`?page=${start - 1}`}>이전</Link>
</li>
{[...Array(pageCount)].map((a, i) => (
<>
{start + i <= totalPages && (
<li key={i}>
<Link className={`${styles.page} ${currentPage === start + i && styles.active}`}
to={`?page=${start + i}`}>
{start + i}
</Link>
</li>
)}
</>
))}
<li className={`${styles.move} ${noNext && styles.invisible}`}>
<Link to={`?page=${start + pageCount}`}>다음</Link>
</li>
</ul>
</div>
);
}
/* pagination.module.css */
ul {
list-style: none;
}
li {
float: left;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.wrapper {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: 30px;
color: #888;
font-size: 14px;
}
.wrapper a {
height: 25px;
line-height: 25px;
}
.page {
margin: 0 5px;
cursor: pointer;
width: 25px;
border-radius: 30px;
border: solid 1px rgba(0, 0, 0, 0);
text-align: center;
}
.page:hover {
border: solid 1px #aaa;
}
.icon, .move:last-child::after, .move:first-child::before {
position: absolute;
font-size: 20px;
padding: 0 7px 0px;
}
.move {
position: relative;
cursor: pointer;
margin: 0 10px;
}
.move a {
width: 50px;
display: block;
z-index: 10;
}
.move a:hover {
text-decoration: underline;
}
.move:first-child {
text-align: right;
}
.move:first-child::before {
content: "<";
left: 0;
}
.move:last-child::after {
content: ">";
right: 0;
}
.invisible {
visibility: hidden;
}
.active {
font-weight: 700;
background: #2f5d62;
color: white;
}
(scss 작업물에서 변환된 코드를 그대로 가져온 것입니다. 사용 환경에 따라 일부 개선이 필요할 수 있습니다.)
사용 예시
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { Book } from "../types/types";
import BookItem from "../components/BookItem";
import Pagination from "../components/Pagination";
export default function Recommend() {
const [totalItems, setTotalItems] = useState(0);
const [books, setBooks] = useState<Book[] | null>(null);
const [searchParams] = useSearchParams();
const page = searchParams.get("page");
useEffect(() => {
window.scrollTo(0, 0); // 페이지 이동 시 스크롤 위치 맨 위로 초기화
/* api 호출 및 데이터(totalItems, books) 저장 */
}, [page]);
return (
<div className="wrapper">
<div className="list-wrapper">
<div className="list">
{books && (
<>
{books.map((book, i) => (
<BookItem book={book} key={i} />
))}
</>
)}
</div>
<Pagination
totalItems={totalBooks}
currentPage={page && parseInt(page) > 0 ? parseInt(page) : 1}
pageCount={5}
itemCountPerPage={50}
/>
</div>
</div>
);
}
(녹화를 위해 window.scrollTo(0, 0) 부분을 주석처리 해두었습니다.)
'Develop > React' 카테고리의 다른 글
[React] useReducer (0) | 2023.09.11 |
---|---|
[React] React Router - RouterProvider, createBrowserRouter (0) | 2023.09.11 |
[React] CSS 없이 간단한 별점 기능(Star Rating) 구현하기 (0) | 2023.07.27 |
[React] textarea 입력에 따라 높이 자동 조절하기 (0) | 2023.07.22 |
[React] memo / useMemo (0) | 2023.02.23 |