Next.js를 사용해 블로그를 개발하는 프로젝트를 진행하고 있다. 홈 화면 하단에 페이지네이션을 추가하여 가장 최근 게시물부터 한 페이지에 3개씩 게시물을 표시해야 하는 상황이다. 이 글에서 내가 공부한 방법을 바탕으로 이를 구현하는 방법을 적어보려고 한다. 코드를 바로 보도록 하자.
블로그의 content들을 담을 MainContent 컴포넌트
"use client";
import React, { useState } from "react";
import ContentContainer from "../contentContainer/ContentContainer";
import styles from "./MainContent.module.scss";
import Pagination from "@/components/molecules/pagination/Pagination";
function MainContent(): JSX.Element {
const [currentPage, setCurrentPage] = useState(1);
const contentsPerPage = 3;
const allContents = [
{ id: 1, userName: "김범수", date: "2023-10-24 00:00" },
{ id: 2, userName: "김범수", date: "2023-10-24 00:01" },
{ id: 3, userName: "김범수", date: "2023-10-24 00:02" },
{ id: 4, userName: "김범수", date: "2023-10-24 00:03" },
{ id: 5, userName: "김범수", date: "2023-10-24 00:04" },
{ id: 6, userName: "김범수", date: "2023-10-24 00:05" },
{ id: 7, userName: "김범수", date: "2023-10-24 00:06" },
// 데이터 예시... api가 아직 없음 ㅠㅠ
];
const indexOfLastContent = currentPage * contentsPerPage;
const indexOfFirstContent = indexOfLastContent - contentsPerPage;
const currentContents = allContents
.slice(0)
.reverse()
.slice(indexOfFirstContent, indexOfLastContent);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
return (
<>
<div className={styles.container}>
<div className={styles.latestContentText}>최신 글</div>
{currentContents.map((content, index) => (
<ContentContainer key={index} content={content} />
))}
<Pagination
currentPage={currentPage}
totalContents={allContents.length}
paginate={paginate}
contentsPerPage={contentsPerPage}
/>
</div>
</>
);
}
export default MainContent;
페이지네이션 기능을 구현할 Pagination 컴포넌트
"use client";
import Link from "next/link";
import styles from "./Pagination.module.scss";
import React from "react";
interface IPaginationProps {
currentPage: number;
totalContents: number;
paginate: (pageNumber: number) => void;
contentsPerPage: number;
}
function Pagination({
currentPage,
totalContents,
paginate,
contentsPerPage,
}: IPaginationProps): JSX.Element {
const pageNumbers: number[] = [];
for (let i = 1; i <= Math.ceil(totalContents / contentsPerPage); i++) {
pageNumbers.push(i);
}
return (
<>
<nav className={styles.container}>
<ul className={styles.pagination}>
{pageNumbers.map((number) => (
<li
key={number}
className={`${styles.pageList} ${
currentPage === number ? styles.active : ""
}`}
>
<Link
href="/"
onClick={() => paginate(number)}
className={styles.pageLink}
>
{number}
</Link>
</li>
))}
</ul>
</nav>
</>
);
}
export default Pagination;
코드를 하나 하나 살펴보도록 하자.
// MainContent.tsx...
const [currentPage, setCurrentPage] = useState<number>(1);
const contentsPerPage = 3;
초기 설정
먼저, 현재 페이지 번호를 나타내는 상태(currentPage)와 한 페이지에 표시할 컨텐츠 수(contentsPerPage)를 정의한다. useState 훅을 사용하여 currentPage의 초기값을 1로 설정한다.
// MainContent.tsx...
const allContents = [
{ id: 1, userName: "김범수", date: "2023-10-24 00:00" },
{ id: 2, userName: "김범수", date: "2023-10-24 00:01" },
{ id: 3, userName: "김범수", date: "2023-10-24 00:02" },
{ id: 4, userName: "김범수", date: "2023-10-24 00:03" },
{ id: 5, userName: "김범수", date: "2023-10-24 00:04" },
{ id: 6, userName: "김범수", date: "2023-10-24 00:05" },
{ id: 7, userName: "김범수", date: "2023-10-24 00:06" },
// 데이터 예시... api가 아직 없음 ㅠㅠ
];
컨텐츠 데이터 준비:
다음으로, 표시할 컨텐츠 데이터 배열 allContents를 준비한다. 원래는 api를 이용해 데이터를 받아와야 하지만, 아직 api가 완성되지 않은 관계로 객체 배열 데이터를 임의로 설정해주었다.
// MainContent.tsx...
const indexOfLastContent = currentPage * contentsPerPage;
const indexOfFirstContent = indexOfLastContent - contentsPerPage;
const currentContents = allContents
.slice(0)
.reverse()
.slice(indexOfFirstContent, indexOfLastContent);
페이지에 맞는 컨텐츠 계산:
indexOfLastContent: 이 값은 현재 페이지에서 보여줄 마지막 컨텐츠의 인덱스를 계산한다. 현재 페이지 번호(currentPage)와 한 페이지에 보여줄 컨텐츠의 수(contentsPerPage)를 곱하여 얻는다.
indexOfFirstContent: 이 값은 현재 페이지에서 보여줄 첫 번째 컨텐츠의 인덱스를 계산한다. indexOfLastContent에서 한 페이지에 보여줄 컨텐츠의 수(contentsPerPage)를 빼서 계산한다.
currentContents: 이 배열은 현재 페이지에 표시할 컨텐츠들만 포함한다. allContents 배열에서 slice(0)를 사용하여 전체 컨텐츠 배열의 복사본을 만든 후, reverse()를 사용하여 배열의 순서를 뒤집는다. 이렇게 하면 최신 컨텐츠가 배열의 앞쪽에 위치하게 된다. 그 다음, slice(indexOfFirstContent, indexOfLastContent)를 사용하여 현재 페이지에 표시할 컨텐츠의 부분 배열을 만든다.
이해를 돕기 위한 예시
총 컨텐츠는 50개로, ID가 1부터 50까지 부여되어 있다.
페이지 당 표시되는 컨텐츠 수는 10개.
우리가 보고자 하는 페이지는 3페이지.
indexOfLastContent 계산:
현재 페이지 번호(3)에 페이지 당 컨텐츠 수(10)를 곱한다.
3 * 10 = 30, 따라서 indexOfLastContent는 30이 된다.
indexOfFirstContent 계산:
indexOfLastContent(30)에서 페이지 당 컨텐츠 수(10)를 뺀다.
30 - 10 = 20, 따라서 indexOfFirstContent는 20이 된다.
currentContents 추출:
기존 allContents는 [1, 2, 3, ..., 50]의 순서로 있다.
reverse()를 적용하면, 배열이 [50, 49, 48, ..., 1]로 뒤집힌다.
slice(indexOfFirstContent, indexOfLastContent)를 적용하면, 인덱스 29부터 20까지의 컨텐츠, 즉 [30 29 28 ... 21]을 추출하게 된다.
(배열의 인덱스는 0부터 시작하기 때문에, 컨텐츠의 ID와 인덱스는 1씩 차이가 남.)
원본 배열:
1 2 3 ... 19 [20 21 22 ... 29] 30 ... 48 49 50
뒤집힌 배열:
50 49 48 ... 31 [30 29 28 ... 21] 20 ... 3 2 1
추출된 컨텐츠 ([ ] 안의 부분):
30 29 28 ... 21
// MainContent.tsx...
const paginate = (pageNumber: number) => {
setCurrentPage(pageNumber);
};
페이지네이션 컨트롤:
Pagination 컴포넌트가 페이지 번호를 표시하고, 사용자가 페이지 번호를 클릭할 때 currentPage 상태를 업데이트하기 위한 paginate 함수를 선언한다.
// Pagination.tsx...
const pageNumbers: number[] = [];
for (let i = 1; i <= Math.ceil(totalContents / contentsPerPage); i++) {
pageNumbers.push(i);
}
전체 페이지 수를 계산:
pageNumbers는 페이지 번호들을 저장할 빈 배열이다. 이 배열에는 각 페이지에 해당하는 숫자들이 들어갈 예정이다.
for 루프는 전체 페이지 수를 계산하여 각 페이지에 해당하는 숫자를 pageNumbers 배열에 채워 넣는다. Math.ceil 함수는 전체 컨텐츠 수를 페이지 당 컨텐츠 수로 나눈 후, 그 결과를 올림 처리하여 총 페이지 수를 구한다. 그리고 'i'는 1부터 시작해서 총 페이지 수까지 1씩 증가하며, 각 'i' 값을 'pageNumbers' 배열에 추가한다.
페이지네이션 로직에서 사용한 JavaScript의 slice, Math.ceil, 그리고 배열의 push 메소드에 대해 좀 더 자세히 알아보자. 이들은 데이터를 조작하고 계산하는 데 있어 중요한 역할을 하며, 본 블로그의 페이지네이션 예제에서도 핵심적인 기능을 담당한다.
용도:
slice 메소드는 배열의 일부분을 추출하여 새로운 배열 객체를 생성한다. 이 메소드는 원본 배열을 수정하지 않는다.
예제 코드:
array.slice(start, end);
// start를 포함한 end 이전(end는 미포함)까지의 배열요소들을 추출하여 새 배열로 생성
const numbers = [1, 2, 3, 4, 5];
const extractedNumbers = numbers.slice(2, 4);
// 2와 4는 인덱스 번호... 즉, start는 3, end는 5
console.log(extractedNumbers); // [3, 4]
본 블로그에서의 사용:
slice 메소드는 현재 페이지에 표시할 컨텐츠의 범위를 결정하는 데 사용된다. allContents 배열에서 필요한 컨텐츠를 추출하여 새로운 배열 currentContents를 생성하는 데 필요한 작업이다.
const currentContents = allContents
.slice(0)
.reverse()
.slice(indexOfFirstContent, indexOfLastContent);
여기서 allContents.slice(0)는 [...allContents]로도 변경이 가능하다.
용도:
Math.ceil 함수는 주어진 숫자보다 크거나 같은 가장 작은 정수를 반환한다. 기본적으로, 숫자를 올림 처리한다.
예제 코드:
console.log(Math.ceil(4)); // 4
console.log(Math.ceil(4.01)); // 5
console.log(Math.ceil(4.99)); // 5
본 블로그에서의 사용:
이 함수는 전체 페이지 수를 계산할 때 사용된다. 전체 컨텐츠 수를 페이지당 표시할 컨텐츠 수로 나누고, 그 결과를 올림하여 실제 필요한 페이지 수를 결정한다.
for (let i = 1; i <= Math.ceil(totalContents / contentsPerPage); i++) {
pageNumbers.push(i);
}
용도:
push 메소드는 배열의 끝에 하나 이상의 요소를 추가하고, 배열의 새로운 길이를 반환한다.
예제 코드:
const fruits = ["apple", "banana"];
fruits.push("orange");
console.log(fruits); // ["apple", "banana", "orange"]
본 블로그에서의 사용:
페이지 번호를 생성하기 위해 push 메소드를 사용한다. 페이지 번호를 담고 있는 배열 pageNumbers에 새 페이지 번호를 추가하여, 사용자에게 페이지네이션 컨트롤을 표시한다.
for (let i = 1; i <= Math.ceil(totalContents / contentsPerPage); i++) {
pageNumbers.push(i);
}
React에서 페이지네이션을 구현하는 것은 사용자 경험을 향상시키고 데이터 로딩 시간을 줄이는 효율적인 방법이다. 본 블로그에서 제시한 방법을 사용하면, 큰 데이터 세트도 사용자가 쉽게 탐색하고 원하는 정보를 빠르게 찾을 수 있다.