react

[React] pagination 컴포넌트 만들기(라이브러리x)

tea-tea 2024. 1. 9. 01:19

이 글은 페이지네이션 컴포넌트를 만드는 법에 대해 다룬다. 

페이지네이션이란, 대량의 데이터를  보여줄 때 데이터를 전부 요청하지 않고 일부만 요청하는 방식이다. 예컨대, 쇼핑몰에서 칫솔을 검색하면 수 만개의 상품 중에서 20개 정도만 보여주고 나머지는 다음 페이지를 클릭할 때 보여준다. 이 방식이  왜 필요할까? 만약 유저가 페이지를 요청할 때 마다 대량의 데이터를 모두 가져온다면, 시간이 많이 들 뿐만 아니라 db에서 데이터를 처리하는 비용까지 크기 때문이다. 

  

페이지네이션은 일반적으로 두 가지 방법이 사용된다.

  • 무한 스크롤:  스크롤을 일정 수준 내리면 추가적인 상품 데이터를 요청하는 방식이다.
  • 버튼 컴포넌트:  유저가 페이지 선택 버튼을 직접 클릭하는 방식이다. 이 글은 후자를 다룰 예정이다. 아래의 예시를 보자.

 

이외에도 다른 방법들이 있겠지만,  공통점은 데이터 요청 url의 쿼리스트링을 변경해서 데이터 수량 스킵량을 설정하는 것이다. 예컨대, 상품 데이터를 요청하는 다음 url이 있다. "api/product?page=1"
여기서 쿼리스트링은 page다. 페이지네이션은 유저의 상호작용(이동 버튼 클릭 or 스크롤) 시 쿼리스트링을 바꿔서 데이터를 요청해야 한다.

 

이 글에서는 버튼 컴포넌트 만드는 방법에 대해 알아볼 것이다. 프레임워크는 nextJS를 사용했고 라이브러리는 SWR을 사용하였다.

 

버튼 컴포넌트 UI

 

 

 

 

1. UI
  • 페이지 이동버튼:  1,2,3,4..10 처럼 페이지를 선택하는 버튼이다. 클릭 시, url의 쿼리스트링 변수(page)가 변경되며 새로운 데이터를 요청한다.
  • 화살표 버튼:  왼쪽 화살표와 오른쪽 화살표가 있는데,  오른쪽 화살표를 클릭하면 페이지가 10개 이상일 때 다음 페이지(11,12...20)로 넘어간다. 왼쪽 화살표는 반대의 역할을 한다.

 

 

2. 상수, 변수, state
  • 한 페이지에서 보여줄 데이터의 최대 숫자
  • 페이지 이동버튼의 개수
  • 데이터의 총 개수
  • 페이지 제어 상태값: useState, 페이지 이동 버튼이 1~10만큼 표시될지 혹은 11~20이나 그 이상을 보여줄지 결정한다.
  • 방향 상태 값: useState, 어떤 화살표 버튼을 눌렀는지 저장한다.

 

 

3. 로직: 페이지 이동 버튼

 

 

먼저 페이지 이동을 위한 버튼을 만들어보자.

이 예시에서는 페이지 이동 버튼을 1부터 10까지 10개로 구성했고 array.map으로 컴포넌트를 생성했다. 각 버튼은 index를 쿼리스트링으로 삼아서 이동한다. 즉, 1번을 누르면 page=1로 이동되고, 2번을 누르면 page=2로 이동될 것이다.

 

  const [pageState, setPageState] = useState(0);

//PAGE_BUTTON_COUNT =10
//pageState의 역할은 다음 섹션에서 다룸 
{Array.from(Array(PAGE_BUTTON_COUNT).keys()).map((element, index) => {
          const pageElement = pageState * PAGE_BUTTON_COUNT + element + 1;

          return (
            <Link
              href={`/lives?page=${pageState * PAGE_BUTTON_COUNT + index + 1}`}
              key={index}
            >

            
              <div>
                <span>{pageElement}</span>
              </div>
            </Link>
          );
        })}

 

 

 

4. 로직: 화살표 버튼

위 예시는 필요한 페이지가 10개 이하일 때 괜찮지만 그 이상일 때는 문제가 생긴다. 이럴 때, 10개 이후의 페이지로 넘어갈 수 있는 화살표 버튼이 필요하다.

 

작동과정은 이러하다.

 

  1.  useState로 pageState를 만들어, 페이지 버튼 그룹이 1~10, 혹은 11~20이나 이상을 보여줄지 결정한다.
  2. useState로 pageDirection을 만들어, 유저가 누른 버튼이 왼쪽 화살표인지 오른쪽 화살표인지를 저장한다.
  3. 화살표 버튼을 클릭하면, onClick 이벤트에서 changePageState를 실행한다. 이 함수는 화살표의 방향을 pageDirection 상태값에 저장하고 pageState를 증가시키거나 감소시킨다.
  4. pageState가 변하면, useEffect를 이용해서 새로운 url로 이동한다.

pageState는 페이지 이동 버튼을 렌더링하는 데 중요한 상태값이다. 이 값이 0이면, 1~10이 렌더링되고, 1이면 11~20이 렌더링되는 방식이다.

//PAGE_BUTTON_COUNT =10   
  const [pageDirection, setPageDirection] = useState();
  const [pageState, setPageState] = useState(0);
  const router = useRouter();
  //nextJS의 훅. url을 변경하기 위함
  
    // 화살표의 방향에 따라서 pageState를 증감
  const changePageState = function (event, direction) {
    setPageDirection(direction);
    setPageState((prev) =>
      direction == "right" ? prev + 1 : prev === 0 ? 0 : prev - 1,
    );
  };

    // pageState가 변하면, 거기에 맞춰서 url 이동
 useEffect(() => {
  if (pageDirectionState !== undefined) {
  // 이 조건문이 없으면, 최초의 페이지 렌더링 시, router.push가 발생하며 오류가 생긴다.
      setPageDirectionState(undefined);
      router.push(
        `${pathname && router.pathname}?${query}=${
          pageState * pageButtonCount + 1
        }`,
      );
    }
  }, [pageState, router ]);




return (
 <section className="pagination px-16 ">
      <div>
        <div
   	      id="페이지네이션 왼쪽 화살표"
          onClick={(event) => changePageState(event, "left")}
        >
          <span>왼쪽 화살표</span>
        </div>
       
       // 페이지 이동 버튼(pageState의 값에 따라서 버튼에 표시된 숫자가 달라짐)
       // 1~10 혹은 11~20 등

          <div
   	        id="페이지네이션 오른쪽 화살표"
            onClick={(event) => changePageState(event, "right")}
          >
            <span>오른쪽 화살표</span>
          </div>
      </div>
    </section>
    );

 

 

 

4. 로직: 현재 url 쿼리스트링을 pageState에 할당하기

이 로직은 page가 11이상인 상황에서 새로고침을 하거나, url 검색으로 처음부터 11이상의 페이지로 들어오는 경우를 대비하여 pageState를 재할당한다. 

 

//PAGE_BUTTON_COUNT =10   
  const [pageDirection, setPageDirection] = useState();
  const [pageState, setPageState] = useState(0);
  const router = useRouter();
  
  // 이 섹션에서 추가된 부분
  // 쿼리스트링 page 변수를 찾아서 pageState에 할당함
  useEffect(() => {
    if (router.query.page) {
      const page = +router.query.page.toString();
      if (isNaN(page)) router.push(`${pathname}?${query}=1`);

      if (page % 10 == 0) {
        setPageState(page / 10 - 1);
      } else {
        setPageState(Math.floor(page / 10));
      }
      setIsRenderState(true);
    }
  }, [router, setPageState, setIsRenderState]);
  
    // 이전 섹션에서의 기존 부분
    // 화살표의 방향에 따라서 pageState를 증감
  const changePageState = function (event, direction) {
    setPageDirection(direction);
    setPageState((prev) =>
      direction == "right" ? prev + 1 : prev === 0 ? 0 : prev - 1,
    );
  };

    // pageState가 변하면, 거기에 맞춰서 url 이동
 useEffect(() => {
    setPageDirection(undefined);

    if (newPage) router.push(`/lives?page=${pageState*PAGE_BUTTON_COUNT+1}`);
        //PAGE_BUTTON_COUNT =10   
  }, [pageState, router, pageDirection]);


return (
 <section className="pagination px-16 ">
      <div>
        <div
   	      id="페이지네이션 왼쪽 화살표"
          onClick={(event) => changePageState(event, "left")}
        >
          <span>왼쪽 화살표</span>
        </div>
       
       /// 페이지 이동 버튼(pageState의 값에 따라서 버튼에 표시된 숫자가 달라짐)
       /// 1~10 혹은 11~20 등

          <div
   	        id="페이지네이션 오른쪽 화살표"
            onClick={(event) => changePageState(event, "right")}
          >
            <span>오른쪽 화살표</span>
          </div>
      </div>
    </section>
    );

 

 

5. 로직:  화살표 버튼 제한하기

이번에는 두 가지 기능을 만들 것이다.

  • 1페이지일 때 왼쪽 화살표 버튼을 작동하지 않기
  • 마지막 페이지일 때 오른쪽 화살표 작동하지 않기

 

이를 위해서는 두가지가 필요하다.

  • 한 페이지당 보여줄 데이터 숫자(ITEM_PER_PAGE)
  • 백엔드에서 보내줄 수 있는 데이터의 총 개수(countTotal)

 

예컨대,  한 페이지당 보여줄 데이터 숫자가 25개이고, 데이터의 총 개수는 100개라면, 페이지는 100/25 =>4개가 필요하다. 하지만, 101개라면 5페이지가 필요하다.

 

  // countTotal = 데이터의 총 개수
  // itemPerPage = 한 페이지당 보여줄 최대 데이터 개수
  
  function (countTotal, itemPerPage) {
    const countTotalPage =
      countTotal === undefined
        ? undefined
        : countTotal % itemPerPage === 0
          ? Math.floor(countTotal / itemPerPage)
          : Math.floor(countTotal / itemPerPage) + 1;

    return countTotalPage;
  },
  
  // 데이터가 있는 페이지의 개수
  const countTotalPage = countTotalPage(countTotal, ITEM_PER_PAGE);
  
  
  ///...
  /// 페이지 이동버튼의 표시 숫자가 countTotalPage보다 클 경우, 생성을 취소
  /// 현재 페이지 버튼에서 제일 마지막 페이지까지 표시된다면, 오른쪽 화살표를 렌더링 하지 않기
   {Array.from(Array(PAGE_BUTTON_COUNT).keys()).map((element, index) => {
          const pageElement = pageState * PAGE_BUTTON_COUNT + element + 1;
          if (!countTotalPage || pageElement > countTotalPage) return;

          return (
            <Link
              href={`/lives?page=${pageState * PAGE_BUTTON_COUNT + index + 1}`}
              key={index}
            >
              <div>
                <span>{pageElement}</span>
              </div>
            </Link>
          );
        })}
        
        {countTotalPage &&
        pageState * PAGE_BUTTON_COUNT + PAGE_BUTTON_COUNT >
          countTotalPage ? null : (
          <div>오른쪽 화살표</div>
        )}

 

페이지네이션을 구현하는 데 필수적인 로직은 여기까지다.

 

 

6. 추가 로직: 화살표 버튼 클릭 시 이동

이 로직은 개인적으로 추가하면 좋을 것 같아 넣어봤다. 예컨대  10개의 페이지 이동 버튼이 있다면 이런 식이다.

 

  •  오른쪽 화살표 버튼을 클릭하면, 11~20 중에서 가장 첫번째 숫자인 11로 이동하고, 다시 오른쪽 화살표를 클릭하면 21~30 중에서 21페이지로 이동한다.
  • 왼쪽 화살표를 클릭하면, 11~20 중 20페이지로 이동하고, 다시 왼쪽을 클릭하면 1~10중 10으로 이동한다.
  • 1~10까지 버튼이 있을 때는 왼쪽 화살표 클릭 시, 1로 이동하고 싶었다.

 

다시 정리해보면 이렇다.

  • 오른쪽 화살표 클릭 시, 다음 페이지 이동 버튼 중에서 가장 첫번째 페이지로 이동한다.
  • 왼쪽 화살표 클릭 시, 이전 페이지 이동 버튼 중에서 가장 마지막 페이지로 이동한다.
  • 현재 페이지가 1~10 중 하나일 때는 1페이지로 이동한다.

 

이 부분은 아래처럼 로직을 구현했다.

export const pagination = {
// 수동 페이지네이션에서 화살표 클릭 시 쿼리스트링 page 값을 계산
  calculateQueryStringPage: function ({
    pageDirection,
    pageState,
    queryStringPage,
    pageButtonCount,
  }) {
    let newPage;

    switch (pageDirection) {
      case "right":
        newPage = pageState * pageButtonCount + 1;
        break;
      case "left":
        if (pageState !== 0) {
          newPage = pageState * pageButtonCount + pageButtonCount;
          break;
        }
        if (pageState === 0) {
          newPage =
            queryStringPage && +queryStringPage > pageButtonCount + 1
              ? pageButtonCount
              : 1;
          break;
        }
        newPage = undefined;
        break;
    }

    return newPage;
  },
};

  useEffect(() => {
    const newPage = pagination.calculateQueryStringPage({
      pageDirection,
      pageState,
      queryStringPage: router.query.page,
      pageButtonCount: PAGE_BUTTON_COUNT,
    });

    setPageDirection(undefined);
    if (newPage) router.push(`/lives?page=${newPage}`);
  }, [pageState, router, pageDirection]);

 

 

6. 정리, 백엔드 로직(참고)

페이지네이션은 다양한 구현방식이 있지만, 프론트엔드의 기본 역할은 쿼리스트링을 이용해서 가져올 데이터 수량스킵량을 설정하는 것이다.

 

그렇다면 백엔드에서는 이를 어떻게 처리할까?

나의 경우에는 next js의 page router을 사용하면서 쿼리스트링의 유효성을 검사하였다.

유효성 검사의 체크 항목은 이렇다.

 

  • 쿼리스트링이 비었거나 공백문자로 구성되지 않았는가
  • 쿼리스트링이 양의 정수가 아닌 문자열로 구성되지 않았는가

 

 // req = request
  const page = String(req.query.page).trim();

    if (page === "" || isNaN(+page) || +page <= 0 || Number(page) % 1 !== 0)
      return res.status(400).json({ ok: false });
  // 쿼리스트링에 문제가 있을 시 작업 중단
  
  
  // 데이터 쿼리 코드 ...

 

 

 


직접 만든 페이지네이션 코드

https://github.com/charchar111/carrot-market-clone2/blob/main/src/components/pagination.tsx

 

페이지네이션 중 무한스크롤에 대해 궁금하다면?

2024.02.19 - [react] - [React] pagination: 무한 스크롤 구현하기(+SWR 라이브러리)