보기 편하게 준비했어 [프로젝트에 라이브러리없이 페이지네이션 적용해보기 그리고 성능 최적화]

승환입니다·2023년 12월 11일
post-thumbnail

왜 라이브러리 없이?

나는 이전 프로젝트는 물론 현재 프로젝트까지 react-paginate 라이브러리를 활용해서 페이지네이션을 구현했었다.

하지만 이번 기회에 라이브러리없이 리팩토링을 결심했다.
결심한 이유는 크게 두가지이다.

첫번째

결과적으로 에러없이 잘 구현했지만 문득 페이지네이션의 내부 구현방식이 궁금했다.

두번째

개발 속도를 높이기위해 라이브러리에 의존했던 나를 뒤돌아보며 기본기를 탄탄하기 하기 위해 라이브러리를 사용하지않고 리팩토링을 결심했다.

뭐 부터 해야할까?

일단 라이브러리로 만든 코드를 뜯어냈다.

function Pagination({ onChangePage, totalPages, page }: PaginationProps) {
  return (
    <div>
      <ReactPaginate
        className={`${styles.pagination} ${
          page === 1 ? styles.Pagination_pagination__cjT7I : undefined
        } `}
        pageCount={totalPages}
        pageRangeDisplayed={5}
        marginPagesDisplayed={5}
        onPageChange={({ selected }) => onChangePage(selected + 1)}
        previousLabel={<FaArrowLeft />}
        nextLabel={<FaArrowRight />}
        activeClassName={page === 1 ? undefined : styles.activePage}
      />
    </div>
  );
}

export default Pagination;

리팩토링 전 코드이다.
일단 ReactPaginate 컴포넌트를 주석처리하고 내가 페이지네이션을 구현하기위해 필요한 정보가 무엇이 있을까 생각해보았다.

페이지네이션을 위해서 내가 알아야하는 정보

  • 현재 페이지
  • 토탈 페이지 수
  • 페이지를 바꿀수있는 함수

이 세개가 필요했다.

내가 구현해야 할 기능

  • 숫자를 누르면 그 페이지를 focus를 해주고 페이지 api를 호출해준다.
  • 이전 , 다음 페이지에 정보가 있다면 화살표를 누를 수 있고 누른다면 다음 페이지가 아닌 다음 구간으로 점프한다.

내가 고민한 구간

마지막 페이지인건 어떻게 내가 알 수 있을까?
마지막페이지 번호가 꼭 5개가 된다는 보장이 안되는데 (밑에 번호가 5개씩 나오도록 설계했다) 계속 5개씩 보여준다고 짜면 오류가 나지 않을까?

  const [currentPage, setCurrentPage] = useState<number[]>([]);

  const initPageNumber = useCallback((totalPages: number, page: number) => {
    const viewPages = [];
    for (let i = page; i <= Math.min(page - 1 + LIMIT, totalPages); i += 1) {
      viewPages.push(i);
    }
    setCurrentPage(viewPages);
  }, []);

Math.min(page - 1 + LIMIT, totalPages)
이 부분에서 마지막 구간에 페이지가 몇개 있는지 판단했다.
만약 마지막페이지가 2개밖에 보여줄게없다면 totalPage가 page -1 + LIMIT보다 작아서 번호가 2개만 나올것이다.
또한 마지막페이지는 totalPages를 가지고있어서 같이 잘 해결되었다.

전체 코드

import React, { useCallback, useEffect, useState } from 'react';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import styles from './index.module.scss';

type PaginationProps = {
  onChangePage: (page: number) => void;
  totalPages: number;
  page: number;
};
const LIMIT = 5;
function Pagination({ onChangePage, totalPages, page }: PaginationProps) {
  const [currentPage, setCurrentPage] = useState<number[]>([]);

  const initPageNumber = useCallback((totalPages: number, page: number) => {
    const viewPages = [];
    for (let i = page; i <= Math.min(page - 1 + LIMIT, totalPages); i += 1) {
      viewPages.push(i);
    }
    setCurrentPage(viewPages);
  }, []);

  const prePages = () => {
    onChangePage(currentPage[0] - 5);
    initPageNumber(totalPages, currentPage[0] - 5);
  };

  const nextPages = () => {
    onChangePage(currentPage[0] + 5);
    initPageNumber(totalPages, currentPage[0] + 5);
  };

  useEffect(() => {
    initPageNumber(totalPages, page);
  }, []);

  return (
    <div className={styles.pagination}>
      <button disabled={currentPage[0] === 1} onClick={prePages} type="button">
        <FaArrowLeft color="#f57a00" />
      </button>
      <ul>
        {currentPage?.map((v) => {
          return (
            <li
              className={v === page ? styles.activePage : undefined}
              onClick={() => onChangePage(v)}
              key={v}
            >
              {v}
            </li>
          );
        })}
      </ul>
      <button
        disabled={currentPage[currentPage.length - 1] === totalPages}
        onClick={nextPages}
        type="button"
      >
        <FaArrowRight color="#f57a00" />
      </button>
    </div>
  );
}

export default Pagination;

결과

추가로 성능 최적화

페이지를 이동할 때 사용자 입장에서 첫번째 페이지를 눌렀을 때
그 다음 가장 누를 확률이 가장 큰 버튼인 두번째 페이지라고 생각했다.
나는 사용자가 페이지 번호를 클릭하면 클릭한 다음 페이지 게시글 데이터들을 미리 가져와 캐싱하면 사용자가 좀 더 빠르게 페이지를 볼 수 있지않을까? 라는 생각을 했고 구글링 끝에 react-query의 queryClient.prefetchQuery라는 문법을 사용해 성능을 향상 시켰다.


Prefetching 란?

pre+fetching == 즉 미리 fetching을 하겠다는 뜻이다!
next 버튼을 누르면 currentPage가 바뀌면서 useEffect함수가 실행된다.
useEffect함수는 페이지가 바뀔떄마다 그 페이지의 다음페이지를 미리 fetching해주는 함수다.
fetching을 한다면 데이터가 캐시에 저장이 되기떄문에 로딩없이 바로 임시로 데이터를 보여줄 수 있다.
결론적으로 사용자에게 편리한 ux를 보여줄 수 있다.

페이지가 바뀔떄마다 useEffect를 호출하게 한건 페이지의 수를 바꿔주는 함수가 비동기이기때문에 useEffect로 순서를 맞춰준것이다.

 useEffect(() => {
    if (currentPage < maxPostPage) {
      let nextPage = currentPage + 1;
      queryClient.prefetchQuery(["posts", nextPage], () => {
        fetchPosts(nextPage);
      }); // 미리 캐시에 담아두기 캐시에 데이터가 있어서 임시로 데이터를 보여줄수있음
    }
  }, [currentPage, useQueryClient]);

함수안에 queryClient.prefetchQuery는 pre+fetch의 합성어이다.
미리 fetching을 시도 하겠다라는 뜻이다!
maxPostPage가 10일떄 currentPage가 10이라면 호출이 안되게 막아놨다.
왜냐하면 currentPage가 10 이라면 11의 데이터가 호출되어야하는데 11 데이터는 없기떄문이다.


실제로 적용한 전체 코드

'use client';

import React, { useEffect, useState } from 'react';
import useInput from '@/src/hooks/useInput';
import { SelectValueType } from '@/src/types/search';
import usePage from '@/src/hooks/usePage';
import {
  keepPreviousData,
  useQuery,
  useQueryClient,
} from '@tanstack/react-query';
import { GetBoards } from '@/src/apis/Auction';
import FilterBox from '../FilterBox';
import styles from './index.module.scss';
import UserAuctionData from '../UserAuctionData';

const UserAuction = () => {
  const queryClient = useQueryClient();
  const [searchData, onchange] = useInput();
  const { page, onChangePage } = usePage();
  const [selectValue, setSelectValue] = useState<SelectValueType>({
    area: '',
    city: '',
    category: '',
  });

  const {
    isPending,
    data: Boards,
    refetch,
  } = useQuery({
    queryKey: ['Boards', page] as const,
    queryFn: ({ queryKey }) => {
      const page = queryKey[1];
      const data = { ...selectValue, searchData, page };
      return GetBoards(data);
    },
    placeholderData: keepPreviousData,
  });

  const fetchBoardList = (page: number) => {
    const data = { ...selectValue, searchData, page };

    return GetBoards(data);
  };

  const prefetchNextPosts = (page: number) => {
    queryClient.prefetchQuery({
      queryKey: ['Boards', page + 1],
      queryFn: () => fetchBoardList(page + 1),
    });
  };

  useEffect(() => {
    if (page < Boards?.totalPages) {
      prefetchNextPosts(page);
    }
  }, [page, prefetchNextPosts, useQueryClient]);

  return (
    <main className={styles.auction}>
      <FilterBox
        refetch={refetch}
        searchData={searchData}
        onchange={onchange}
        selectValue={selectValue}
        setSelectValue={setSelectValue}
        onChangePage={onChangePage}
      />
      <UserAuctionData
        page={page}
        Boards={Boards}
        isPending={isPending}
        onChangePage={onChangePage}
      />
    </main>
  );
};

export default UserAuction;

결과

후기

라이브러리없이 페이지네이션을 구현했다.
비록 라이브러리만큼 다양한 기능을 100% 똑같이 구현하진 못했지만 한번이라도 이렇게 자바스크립트를 써보면서 경험해보는것도 좋은 경험이라고 생각한다.
더해서 리액트쿼리를 사용해 성능 최적화까지 눈으로 확인할 수 있었다.
단순한 상황에서도 성능 최적화는 필요하다고 생각이 들었고 그만큼 신경쓰고 공부를 해야겠다는 생각이 들었다.

profile
자바스크립트를 좋아합니다.

3개의 댓글

comment-user-thumbnail
2024년 5월 10일

안녕하세요! 유익한 글 잘 봤습니다~ 디자인도 너무 귀여워서 css는 어떤 식으로 작성했는지 궁금해서요! 혹시 알 수 있을까요~?

1개의 답글