[React] 페이지네이션 구현

wookhyung·2022년 7월 21일
21

React

목록 보기
3/6

📑 페이지네이션 구현하기!

현재 진행 중인 프로젝트에서 메인 페이지를 담당하고 있는데, 페이지네이션 기능이 필수적으로 필요했다. 경기장 데이터가 많이 있고 한 페이지에 카드 형식으로 8개의 경기장 밖에 보여주지 않기 때문에 처음으로 페이지네이션 구현에 도전해봤다.

그리고 구현 전에 찾아보니까 페이지네이션 라이브러리 역시 존재했다. 처음부터 라이브러리를 쓰지 않고 직접 구현을 해보려고 생각은 했는데 그래도 궁금해서 조금 찾아봤다.

📌 페이지네이션 라이브러리

1️⃣ Material-UI

Material-UI에서는 React Pagination component를 제공한다.
https://mui.com/material-ui/react-pagination/#pagination

디자인도 굉장히 훌륭한데, 사용하지 못할 이유가 하나 있다.

Material-UI에서 제공하는 Pagination 컴포넌트는 첫번째 페이지 숫자와 마지막 숫자를 무조건 띄워준다.

내가 구현하고 싶었던 방법은 아래와 같이, 만약 10페이지가 있으면 1~5 페이지를 보여주고 다음으로 넘기면 6~10 페이지를 보여주는 페이지네이션을 구현하고 싶었기 때문에 해당 라이브러리는 부합하지 않는다.

2️⃣ react-js-pagination

https://www.npmjs.com/package/react-js-pagination

A ReactJS dumb component to render a pagination.

The component comes with no built-in styles. HTML layout compatible with Bootstrap 3 pagination stylesheets.

pagination 관련된 패키지 중 다운로드 수가 꽤 많은 라이브러리다. 실제로 구글링을 해보니까 꽤 많이 사용하는 것 같고, 사용하기도 좋은 것 같다. 위에 적혀있듯 기본 스타일도 적용되지 않아서 CSS 변경도 용이해보인다. 나도 아마 다음에 페이지네이션 구현이 필요하다면 충분히 고려해볼 것 같다.

공식문서의 예제는 class component로 작성되어 있어서 함수형의 예제를 찾아서 들고 와봤다.

다음에 사용하게 된다면 아래의 예제를 참고해야겠다.

import React, { useState } from "react";
import './Paging.css';
import Pagination from "react-js-pagination";

const Paging = () => {
  const [page, setPage] = useState(1);

  const handlePageChange = (page) => {
    setPage(page);
  };

  return (
    <Pagination
      activePage={page} // 현재 페이지
      itemsCountPerPage={10} // 한 페이지랑 보여줄 아이템 갯수
      totalItemsCount={450} // 총 아이템 갯수
      pageRangeDisplayed={5} // paginator의 페이지 범위
      prevPageText={"‹"} // "이전"을 나타낼 텍스트
      nextPageText={"›"} // "다음"을 나타낼 텍스트
      onChange={handlePageChange} // 페이지 변경을 핸들링하는 함수
    />
  );
};

export default Paging;

📌 라이브러리 없이 페이지네이션 구현하기

라이브러리는 뒤로 하고, 페이지네이션을 직접 구현해보자.

다른 페이지에도 페이지네이션이 필요한 곳이 있어서 재활용 가능한 컴포넌트로 분리해서 만들었고, 전체 코드는 아래와 같다.

const Pagination = ({ totalPage, limit, page, setPage }) => {
  // 총 페이지 갯수에 따라 Pagination 갯수 정하기, limit 단위로 페이지 리스트 넘기기
  const [currentPageArray, setCurrentPageArray] = useState([]);
  const [totalPageArray, setTotalPageArray] = useState([]);

  useEffect(() => {
    if (page % limit === 1) {
      setCurrentPageArray(totalPageArray[Math.floor(page / limit)]);
    } else if (page % limit === 0) {
      setCurrentPageArray(totalPageArray[Math.floor(page / limit) - 1]);
    }
  }, [page]);

  useEffect(() => {
    const slicedPageArray = sliceArrayByLimit(totalPage, limit);
    setTotalPageArray(slicedPageArray);
    setCurrentPageArray(slicedPageArray[0]);
  }, [totalPage]);

  return (
    <PaginationWrapper>
      <FaAngleDoubleLeft onClick={() => setPage(1)} disabled={page === 1} />
      <FaAngleLeft onClick={() => setPage(page - 1)} disabled={page === 1} />
      <ButtonWrapper>
        {currentPageArray?.map((i) => (
          <PageButton
            key={i + 1}
            onClick={() => setPage(i + 1)}
            aria-current={page === i + 1 ? 'page' : null}
          >
            {i + 1}
          </PageButton>
        ))}
      </ButtonWrapper>
      <FaAngleRight
        onClick={() => setPage(page + 1)}
        disabled={page === totalPage}
      />
      <FaAngleDoubleRight
        onClick={() => setPage(totalPage)}
        disabled={page === totalPage}
      />
    </PaginationWrapper>
  );
};

export default Pagination;

페이지네이션 구현 과정을 조금 나눠서 보면 이렇다.

  1. 데이터 갯수에 따라 최대 페이지 갯수 정하기
  2. 페이지를 선택하거나 다음 방향표를 눌러서 그 페이지에 해당하는 API 요청하는 부분 (offset)
  3. 최대 페이지가 5페이지가 넘어갈 경우, 5페이지 다음으로 넘어가면 6~10 페이지 보여주기

1️⃣ 데이터 갯수에 따라 최대 페이지 갯수 정하기

이 부분은 백엔드 분에게 API 요청 시 전체 데이터 갯수를 알려달라고 했다.

현재 API는 offset를 통하여 8페이지씩 잘라서 가져오기 때문에 전체 데이터 갯수를 알 수 없어 페이지네이션을 위해서 전체 데이터 갯수를 같이 받는다.

Pagination 컴포넌트에서는 totalPage를 전달 받아 totalPage 만큼의 페이지를 구성한다.

const GroundPhotoList = () => {
  const [groundList, setGroundList] = useRecoilState(groundPhotoListState);
  const [page, setPage] = useState(1);
  const listPerPage = 8;
  const totalPage = Math.ceil(groundList.length / listPerPage);

  useEffect(() => {
    (async () => {
      const result = await Api.get(
        `grounds?location=${location}&search=${searchInput}&offset=${
          (page - 1) * listPerPage
        }&count=${listPerPage}`,
      );
      setGroundList({
        length: result.data.length,
        data: result.data.grounds,
      });
    })();
  }, [page]);

  return (
   	  ...
    
      <Pagination
        totalPage={totalPage}
        limit={5}
        page={page}
        setPage={setPage}
      />
    </>
  );
};

export default GroundPhotoList;

위는 Pagination 컴포넌트를 사용하는 경기장 리스트 컴포넌트인데, 먼저 경기장 데이터 API에 요청하여 전체 데이터 갯수를 받아와서 Pagination 컴포넌트에 전체 데이터 갯수에서 한 페이지에 보여줄 갯수를 나누어 보내준다. (페이지네이션 갯수)

const totalPage = Math.ceil(groundList.length / listPerPage);

2️⃣ 페이지를 선택하거나 다음 방향표를 눌러서 그 페이지에 해당하는 API 요청하는 부분 (offset)

현재 방식은 전체 데이터를 다 받아온 뒤에 클라이언트 단에서 자르는게 아니라, 다음 페이지에 넘어갔을 때 그 페이지에 해당하는 데이터들을 새로 받아오는 방식이기 때문에 유저가 페이지를 넘길 때마다 새로운 데이터를 받아올 API를 호출해야 한다.

이 부분은 위의 코드에 나와있듯이 useEffect Hook을 통해 구현했다.

먼저 현재 페이지를 나타내는 page라는 state를 만들었고, page가 변경될 때마다 useEffect의 콜백 함수에서 새로운 데이터를 받아오면서 경기장 리스트를 바꿔준다.

useEffect(() => {
    (async () => {
      const result = await Api.get(
        `grounds?location=${location}&search=${searchInput}&offset=${
          (page - 1) * listPerPage
        }&count=${listPerPage}`,
      );
      setGroundList({
        length: result.data.length,
        data: result.data.grounds,
      });
    })();
  }, [page]);

3️⃣ 최대 페이지가 5페이지가 넘어갈 경우, 5페이지 다음으로 넘어가면 6~10 페이지 보여주기

위의 예시 사진에서 봤듯, 만약 10페이지가 있으면 1~10의 숫자를 다 보여주는게 아닌 1~5, 6~10, 11~15, ... 이런 형태로 배열로 잘라 5페이지에서 다음으로 넘어가면 그 다음 배열인 6~10 페이지를 보여주는 방법으로 구현을 하고 싶었다.

그래서 먼저, Pagination 컴포넌트가 만들어질 때 전체 페이지 갯수를 통해서 배열로 자르는 과정이 필요하다. 자르는 과정은 따로 함수로 만들어서 totalPage와 limit을 이용했다.

예를 들면, totalPage가 17이고 limit이 5이면 sliceArrayByLimit 함수를 통해서 [1, 2, 3, ... 17] 까지의 배열을 먼저 만들고 이 배열을 limit 단위로 잘라 [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [...]] 와 같은 배열을 만든다.

 useEffect(() => {
    const slicedPageArray = sliceArrayByLimit(totalPage, limit);
    setTotalPageArray(slicedPageArray);
    setCurrentPageArray(slicedPageArray[0]);
  }, [totalPage]);

// useful-functions.js
// 페이지네이션 할 때, 특정 숫자까지의 배열을 만들고 limit 기준으로 자른 배열 만들기
export const sliceArrayByLimit = (totalPage, limit) => {
  const totalPageArray = Array(totalPage)
    .fill()
    .map((_, i) => i);
  return Array(Math.ceil(totalPage / limit))
    .fill()
    .map(() => totalPageArray.splice(0, limit));
};

위의 과정을 통해서 페이지네이션에 넣을 배열은 다 만들어졌으므로 다음 배열이나 이전 배열로 넘어가는 로직을 만들어주면 된다.

이 부분에 대해서 어떻게 구현해야 될지 고민을 했는데, 5페이지에서 6페이지로 넘어갈 때 페이지 state는 6이 되고, 내가 정한 페이지네이션 갯수는 5이기 때문에 둘을 나누면 나머지는 1이 된다.

10페이지에서 11페이지로 넘어갈 때도 마찬가지로 11 % 10 = 1이 되기 때문에 이걸 이용해서 다음 배열로 넘겨주고, 반대로 이전 배열로 넘어갈 때는 6페이지가 5페이지가 되면서 나머지가 0이 되기 때문이 이를 이용해 이전 배열로 바꿔준다.

 useEffect(() => {
    if (page % limit === 1) {
      setCurrentPageArray(totalPageArray[Math.floor(page / limit)]);
    } else if (page % limit === 0) {
      setCurrentPageArray(totalPageArray[Math.floor(page / limit) - 1]);
    }
  }, [page]);

결론

라이브러리 없이 페이지네이션을 구현 해봤는데 생각보다 코드도 길어지고, 로직을 생각하는게 까다로운 부분도 있어서 다음에는 라이브러리를 써서 구현 해보는 것도 나쁘지 않다는 생각이 들었다. 그래도 경험 삼아 라이브러리 없이 구현해보는 것도 좋은 경험인 것 같다 🙂

profile
Front-end Developer

0개의 댓글