FE/React

React로 Pagination 구현하기

이숨인 2024. 8. 20. 20:58

프로젝트를 진행하면서 데이터가 많아지면 페이지네이션(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",
});