image

Pagination

태그
JavascriptReact
상세설명pagination 구현하기
작성일자2024.01.08

프로젝트를 진행 하다 보면 리스트 페이지에서 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>
        </>
      );
    }

    결과