페이지네이션(Pagination) 구현하기: 한 페이지에 특정 수의 항목만 표시 (Next.Js-Typescript)

Devinix·2023년 10월 24일
0
post-thumbnail

개요

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 함수, 메소드 좀 더 깊게 알아보기

페이지네이션 로직에서 사용한 JavaScript의 slice, Math.ceil, 그리고 배열의 push 메소드에 대해 좀 더 자세히 알아보자. 이들은 데이터를 조작하고 계산하는 데 있어 중요한 역할을 하며, 본 블로그의 페이지네이션 예제에서도 핵심적인 기능을 담당한다.

slice()

용도:
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()

용도:
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()

용도:
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에서 페이지네이션을 구현하는 것은 사용자 경험을 향상시키고 데이터 로딩 시간을 줄이는 효율적인 방법이다. 본 블로그에서 제시한 방법을 사용하면, 큰 데이터 세트도 사용자가 쉽게 탐색하고 원하는 정보를 빠르게 찾을 수 있다.

profile
프론트엔드 개발

0개의 댓글