React

Pagination

minsun309 2024. 8. 26. 09:26

프로젝트를 진행 하다 보면 리스트 페이지에서 pagination기능을 적용하고는 한다. 따로 라이브러리는 사용하지 않고 자바스크립트로 pagination을 구현했다.

현재 제작한 개인 블로그에서도 한 페이지 당 9개 포스트로 조건 별로 pagination을 구현했다.

기본 pagination

Pagination 설정

  • currentPage : 초기 값 1로 설정하여 첫 페이지부터 보이게 한다.
  • indexOfLast / indexOfFirst ****: 현재 페이지에 따라 보여줄 블로그 목록의 시작과 끝 인덱스를 계산한다.
  • {blogs?.results.slice(indexOfFirst, indexOfLast).map((item: ListResults) => ...} ****: 현재 페이지에 해당하는 블로그 목록을 슬라이스 해 블로그 아이템에 대해 Post 컴포넌트를 렌더링 한다.
  • <Pagination ... /> : 페이지네이션 컴포넌트를 렌더링하고 필요한 props를 전달한다.

 

props

postsPerPage : 각 페이지에 표시되는 블로그 포스트의 수 ( 예시에서는 한 페이지당 9개의 포스트가 보여준다. )

totalPosts : 전체 블로그 포스트의 수

currentPage : 현재 화면에 표시되는 페이지의 번호

setCurrentPage : 페이지를 변경할 때 호출되는 콜백 함수로, 현재 페이지를 업데이트한다.

import Pagination from "@/components/ScreenElement/pagination";

export default function Blog({ blogs }: BlogistObject) {
  // Paging
  const [currentPage, setCurrentPage] = useState(1);
  const indexOfLast = currentPage * 9;
  const indexOfFirst = indexOfLast - 9;

	return (
     <>		.
       ...
       <div>
         {blogs?.results.slice(indexOfFirst, indexOfLast).map((item: ListResults) => (
              <Post
                key={item.id}
                item={item}
                viewStyle={"gallery"}
                tagCategory={"All"}
              />
          ))}
       </div>
       <Pagination
          postsPerPage={9}
          totalPosts={blogs?.results.length || 0}
          currentPage={currentPage}
          setCurrentPage={setCurrentPage}
       />
     </>
  );
}

 

 

Pagination 컴포넌트

  • totalPages : 전체 페이지 수를 계산한다. ( 전체 포스트의 수를 한 페이지에 표시되는 포스트의 수로 나누어 총 페이지 수를 계산함. Math.ceil() 함수는 주어진 숫자를 올림하여 가장 가까운 정수로 반환 )
  • displayPages : 현재 화면에 보여질 페이지 번호들을 저장할 배열
  • sidePageNumbers : 현재 페이지 주변에 표시할 페이지 번호의 개수를 결정한다.

( ex) maxPageNumbers가 5라면, sidePageNumbers는 2가 됩니다. 이는 현재 페이지를 중심으로 좌우로 2개의 페이지 번호를 표시하도록 하는 것입니다. sidePageNumbers 을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위를 동적으로 계산하여 페이지네이션 컴포넌트가 적절한 수의 페이지 번호를 보여준다. Math.floor() 함수는 소수점 이하를 버린다. )

  • startPageNumber / endPageNumber: 화면에 보여질 페이지 번호의 시작과 끝을 계산한다.

if문을 통해 현재 페이지 주변에 표시할 페이지 번호의 범위가 전체 페이지 수를 초과하지 않도록 조건을 걸었다.

  • <BsChevronLeft /> / <BsChevronRight /> : 클릭 시 현재 페이지에서 전, 후 페이지로 이동한다.
  • <BsChevronDoubleLeft /> / <BsChevronDoubleRight /> : 클릭 시 현재 페이지에서 10페이지 전, 10페이지 후로 이동한다.
// pagination.tsx
import { PaginationType } from "@/InterfaceGather";
import { cls } from "libs/utils";
import { BsChevronDoubleLeft, BsChevronDoubleRight, BsChevronLeft, BsChevronRight } from "react-icons/bs";

export default function Pagination({
  postsPerPage,
  totalPosts,
  currentPage,
  setCurrentPage,
}: PaginationType) {
  const totalPages = Math.ceil(totalPosts / postsPerPage);
  const displayPages = [];
  const maxPageNumbers = 5;
  const sidePageNumbers = Math.floor(maxPageNumbers / 2);

  let startPageNumber = currentPage - sidePageNumbers;
  let endPageNumber = currentPage + sidePageNumbers;

  if (startPageNumber <= 0) {
    startPageNumber = 1;
    endPageNumber = Math.min(totalPages, maxPageNumbers);
    // startPageNumber를 1로 설정하여 음수나 0을 피하고, 
    // endPageNumber를 totalPages와 maxPageNumbers 중 작은 값으로 설정하여 
    // 페이지 범위가 최대 페이지 수를 초과하지 않도록 한다.
  }

  if (endPageNumber > totalPages) {
    startPageNumber -= endPageNumber - totalPages;
    endPageNumber = totalPages;
    // startPageNumber를 현재 값에서 초과한 만큼 감소시켜 
    // 페이지 번호가 어긋나지 않도록 조절하고, endPageNumber를 
    // totalPages로 설정하여 페이지 범위가 최대 페이지 수를 초과하지 않도록 합니다.
  }

  for (let i = startPageNumber; i <= endPageNumber; i++) {
    displayPages.push(i);
  }

  const showNum = displayPages.filter((n) => {
    return n > 0;
  });

  return (
    <ul className="pagination-style">
      {currentPage > 10 && (
        <li
          className="arrow aLeftDouble"
          onClick={() => setCurrentPage(currentPage - 10)}
        >
          <BsChevronDoubleLeft />
        </li>
      )}
      {currentPage > 1 && (
        <li
          className="arrow aLeft"
          onClick={() => setCurrentPage(currentPage - 1)}
        >
          <BsChevronLeft />
        </li>
      )}
      {startPageNumber > 1 && (
        <li onClick={() => setCurrentPage(1)}>
          <span>1</span>
        </li>
      )}
      {startPageNumber > 2 && (
        <li className="ellipsis">
          <span>...</span>
        </li>
      )}
      {showNum.map((number: any) => (
        <li
          key={number}
          onClick={() => setCurrentPage(number)}
          className={cls(currentPage === number ? "currentpage" : "")}
        >
          {number}
        </li>
      ))}
      {endPageNumber < totalPages - 1 && (
        <li className="ellipsis">
          <span>...</span>
        </li>
      )}
      {endPageNumber < totalPages && (
        <li onClick={() => setCurrentPage(totalPages)}>
          <span>{totalPages}</span>
        </li>
      )}
      {currentPage < totalPages && (
        <li
          className="arrow aRight"
          onClick={() => setCurrentPage(currentPage + 1)}
        >
          <BsChevronRight />
        </li>
      )}
      {currentPage <= totalPages - 10 && (
        <li
          className="arrow aRightDouble"
          onClick={() => setCurrentPage(currentPage + 10)}
        >
          <BsChevronDoubleRight />
        </li>
      )}
    </ul>
  );
}

 

 

Pagination 타입

// Interface.ts
export interface PaginationType {
  postsPerPage: number;
  totalPosts: number;
  currentPage: number;
  setCurrentPage: React.Dispatch<React.SetStateAction<number>>;
}

 

블로그 적용 사항

기존에 tagCategory 중 하나를 누르면 해당 tag가 포함된 포스트만 필터 되어져 화면에 보여줬다. pagination 기능을 추가하면서 문제가 발생했다.

  • 태그 선택 시 포스트 수 변화
  • 태그 선택 시 첫 페이지부터 시작 필요 ( ex). emotion 태그에서 3번 페이지에 있다가 css 페이지 누르면 css의 1번 페이지가 보여야 한다.
  • 정렬 순서 변경 시 페이지 마다가 아닌 전체 데이터 순서 변경

해결 방법

  • 태그 선택 시 포스트 수 변화

검색 기능에서 사용했던 filteredList 를 활용하여 조건에 해당하는 포스트 만 필터 된 데이터를 filteredList 에 저장하여 useEffect를 활용해 tagCategory 변경 할 때마다 tagCategoryCount() 를 호출한다. filteredList 에 있는 블로그 목록을 블로그 아이템에 대해 Post 컴포넌트를 렌더링 한다.

  • 태그 선택 시 첫 페이지부터 시작 필요

useEffect를 활용해 tagCategory 변경 할 때마다 setCurrentPage(1) 를 주어 현재 페이지 첫 번째 페이지로 업데이트한다.

  • 정렬 순서 변경 시 페이지 마다가 아닌 전체 데이터 순서 변경

블로그에서 포스트 글을 정렬 변경 시 순서가 바꿀 수 있다. 기본에서 처럼 데이터를 미리 slice() 하면 각 페이지내에서만 정렬이 변경된다. 그래서 정렬 함수로 만든 useSortedData() 안에서 Post 컴포넌트를 렌더링하고 렌더링된 데이터를 .slice(indexOfFirst, indexOfLast) 하면 전체 데이터 순서가 변경된다.

 

 const tagCategoryCount = () => {
    let tagCount = blogs.results;

    if (tagCategory !== DEFINE.TAGCATEGORY.ALL) {
      tagCount = blogs.results.filter((item: ListResults) => {
        return item?.properties["태그"].multi_select
          .map((row: any) => row.name)
          .includes(tagCategory);
      });
    }

    if (tagCategory === DEFINE.TAGCATEGORY.ETC) {
      tagCount = blogs.results.filter((item: ListResults) => {
        return item?.properties["태그"].multi_select
          .map((row: any) => row.name)
          .some((i) =>
            [DEFINE.TAGCATEGORY.HTML, DEFINE.TAGCATEGORY.NEXTJS].includes(i)
          );
      });
    }

    setFilteredList(tagCount);
  };

 useEffect(() => {
    tagCategoryCount();
    setCurrentPage(1);
  }, [tagCategory]);

 

 

전체 코드

/* eslint-disable react-hooks/rules-of-hooks */
import Post from "@/components/post";
import { BASE_URL, DATABASE_ID_BLOG, TOKEN } from "libs/config";
import { useSortedData } from "libs/usePageState";
import { useBlogPageStore } from "@/store/pageStore";
import { useEffect, useState } from "react";
import DEFINE from "@/constant/Global";
import { BlogistObject, ListResults } from "@/InterfaceGather";
import Pagination from "@/components/ScreenElement/pagination";

export default function Blog({ blogs }: BlogistObject) {
  const [tagCategory, setTagCategory] = useState<string>(DEFINE.TAGCATEGORY.ALL);
  const [filteredList, setFilteredList] = useState<ListResults[]>([]);

  useEffect(() => {
    setFilteredList(blogs.results);
  }, [blogs.results]);

  // Paging
  const [currentPage, setCurrentPage] = useState(1);
  const indexOfLast = currentPage * 9;
  const indexOfFirst = indexOfLast - 9;

  const tagCategoryCount = () => {
    let tagCount = blogs.results;

    if (tagCategory !== DEFINE.TAGCATEGORY.ALL) {
      tagCount = blogs.results.filter((item: ListResults) => {
        return item?.properties["태그"].multi_select
          .map((row: any) => row.name)
          .includes(tagCategory);
      });
    }

    if (tagCategory === DEFINE.TAGCATEGORY.ETC) {
      tagCount = blogs.results.filter((item: ListResults) => {
        return item?.properties["태그"].multi_select
          .map((row: any) => row.name)
          .some((i) =>
            [DEFINE.TAGCATEGORY.HTML, DEFINE.TAGCATEGORY.NEXTJS].includes(i)
          );
      });
    }

    setFilteredList(tagCount);
  };

  useEffect(() => {
    tagCategoryCount();
    setCurrentPage(1);
  }, [tagCategory]);

  return (
    <>
       <div className="laptop-max-width">
          ....
          <div className="post-content-area">
            ...
            <div className="page-state-style">
              <ul className="item-tagCategory">
                <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.ALL)}>
                  {DEFINE.TAGCATEGORY.ALL}({blogs.results.length})
                </li>
                <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.DEV)}>
                  {DEFINE.TAGCATEGORY.DEV}
                </li>
                ..... // 태그 li
                <li onClick={() => setTagCategory(DEFINE.TAGCATEGORY.ETC)}>
                  {DEFINE.TAGCATEGORY.ETC}
                </li>
              </ul>
              <PageState path={"blogs"} />
            </div>
            <div>
              {useSortedData(
                filteredList.map((item: ListResults) => (
                  <Post
                    key={item.id}
                    item={item}
                    viewStyle={viewStyle}
                    tagCategory={tagCategory}
                  />
                )),
                sortedContent
              ).slice(indexOfFirst, indexOfLast)}
            </div>
            ...
          </div>
          <Pagination
            postsPerPage={9}
            totalPosts={filteredList?.length || 0}
            currentPage={currentPage}
            setCurrentPage={setCurrentPage}
          />
       </div>
    </>
  );
}

 

 

결과