라이브러리 없이 페이지네이션 구현하기 (Feat. Recoil)

LIMHALIM·2025년 1월 9일
1

이번 글에서는 외부 라이브러리 없이, 순수 React와 Recoil을 사용하여 간단한 페이지네이션 컴포넌트를 구현한 방법을 공유합니다.


1. 페이지네이션의 기본적인 구조

페이지네이션은 기본적으로 페이지 번호를 표시하고, 페이지를 이동할 수 있는 버튼(이전, 다음)을 제공합니다. 페이지네이션을 구현하기 위해 필요한 주요 요소는 다음과 같습니다.

현재 페이지 번호(currentPageNum)
시작 페이지 번호(startPageNum)
전체 페이지 수(totalPages)

2. 컴포넌트 설계

페이지네이션 컴포넌트는 startPageNum, currentPageNum, totalPages라는 세 가지 상태 값을 Recoil을 사용해 전역 상태로 관리했습니다. 이를 통해 애플리케이션 내에서 여러 컴포넌트가 동일한 상태 값을 공유할 수 있도록 하였습니다.

3. Recoil을 이용한 상태 관리

startPageNum은 현재 페이지 번호의 시작 값을 저장하고, currentPageNum은 사용자가 현재 보고 있는 페이지 번호를 저장하며, totalPages는 전체 페이지 수를 저장합니다.

import { useRecoilState } from 'recoil';
import {
  currentPageNumState,
  startPageNumState,
  totalPagesState,
} from 'recoil/states';

여기서 useRecoilState를 사용하여 페이지네이션 관련 상태들을 관리하고 있습니다. 예를 들어, 페이지 번호가 변경되면 해당 상태를 업데이트하여 페이지 내용을 동적으로 변경할 수 있습니다.

4. 페이지네이션 버튼과 페이지 번호 생성

페이지네이션에서 중요한 부분은 페이지 번호를 어떻게 표시할 것인지입니다. 여기서는 range 함수로 현재 페이지 범위에 해당하는 번호들을 계산하여 배열로 반환하고, 이를 기반으로 페이지 번호를 출력하였습니다.

const range = (start = 0) => {
  return Array.from({ length: 5 }, (_, index) => index + start);
};

이 코드는 start 페이지 번호부터 시작하여 5개의 페이지 번호를 생성합니다.
예를 들어, startPageNum이 1이면 [1, 2, 3, 4, 5]가 출력되고, startPageNum이 6이면 [6, 7, 8, 9, 10]이 출력됩니다.

5. 페이지 이동 기능 구현

페이지 이동은 이전, 다음 버튼을 클릭하여 페이지 범위를 변경하는 방식으로 구현했습니다. 사용자가 이전 버튼을 클릭하면 startPageNum을 5만큼 감소시키고, 다음 버튼을 클릭하면 5만큼 증가시켜 페이지를 변경합니다.

const handlePrevPage = () => {
  if (startPageNum <= 5) return;
  setCurrentPageNum(startPageNum - 1);
  setStartPageNum(startPageNum - 5);
};

const handleNextPage = () => {
  if (startPageNum + 5 > totalPages) return;
  setCurrentPageNum(startPageNum + 5);
  setStartPageNum(startPageNum + 5);
};

handlePrevPagehandleNextPage 함수는 페이지가 범위를 벗어나지 않도록 조건을 설정하여 불필요한 페이지 이동을 막습니다.

if (startPageNum <= 5) return;

현재 페이지 범위 중 시작하는 번호가 5보다 작은 즉, 1-5의 범위라면 prev 버튼은 동작하지 않습니다. 1-5 다음 범위인 6-10, 11-15 등 의 페이지 범위라면 현재 페이지 번호는 현재 시작 페이지 번호에서 (현재 시작 페이지 번호가 6이라면 5로, 11이라면 10으로) -1을 해주고, 이후 시작 페이지 번호를 -5하여 update 해줍니다. (현재 시작 페이지 번호가 6이라면 1로, 11이라면 6으로)

따라서, 위 두 함수는 prev 버튼 클릭 시 이전 범위의 가장 마지막 페이지로, next 버튼 클릭 시 이후 범위의 가장 첫번째 페이지로 이동하는 함수입니다.

6. 페이지 번호 클릭

페이지 번호를 클릭했을 때 해당 페이지로 이동할 수 있도록 handlePage 함수를 구현했습니다. 이 함수는 클릭한 페이지 번호를 currentPageNum 상태에 저장하여 해당 페이지의 데이터를 로드할 수 있도록 하였습니다.

const handlePage = (e: React.MouseEvent<HTMLElement>) => {
  const { id } = e.target as HTMLElement;
  setCurrentPageNum(Number(id));
};

7. 컴포넌트 통합

Pagination 컴포넌트는 PostList 컴포넌트 내에서 사용됩니다. PostList 컴포넌트는 게시물 리스트와 함께 페이지네이션을 보여주는 역할을 합니다. 전체 페이지 수가 1보다 크면 Pagination 컴포넌트를 렌더링하여 사용자가 페이지를 이동할 수 있도록 합니다.

const PostList = ({ list, totalPages }: { list: IPostPreviewTypes[]; totalPages: number }) => {
  return (
    <S.List>
      {list && list.map(value => (
        <PostItem
          key={value.id}
          id={value.id}
          title={value.title}
          content={value.content}
          hit={value.hit}
          comment={value.comment}
          likes={value.likes}
        />
      ))}
      {totalPages > 1 && <Pagination />}
    </S.List>
  );
};

결과


👁️ 1년이 지난 시점에서 느낀 아쉬운 점 및 개선점

위의 페이지네이션 구현은 기본적인 페이지 이동 기능을 잘 수행하지만, 몇 가지 아쉬운 점과 개선할 부분이 있었습니다.

  1. 상태 관리 최적화
    Recoil을 사용하여 페이지 번호를 관리하는 것은 좋은 접근이였지만, 페이지 이동과 관련된 상태 관리가 조금 중복되는 느낌이 들었다.
    예를 들어, startPageNumcurrentPageNum은 사실 서로 밀접하게 연관되어 있어서 두 개의 상태를 각각 관리하는 것보다는 하나의 상태로 합쳐서 관리하면 더 효율적일 것 같다는 생각을 한다.

    따라서, currentPageNum만을 관리하고, 그 값을 기반으로 startPageNum을 계산하는 방식으로 상태를 통합하면 관리가 간편해지고, 페이지 이동 시 불필요한 렌더링을 줄일 수 있을 것 같다.

  1. 불필요한 렌더링 방지
    현재는 페이지 번호를 사용자가 클릭할 때마다 전체 페이지네이션을 다시 렌더링한다. 이 방식은 데이터가 많아질수록 성능에 영향을 미칠 수 있다고 생각한다.

    따라서, 페이지 번호와 버튼이 불필요하게 리렌더링되지 않도록 React.memo를 활용하여 컴포넌트를 최적화할 수 있을 것 같다. Pagination 컴포넌트와 관련된 상태가 바뀔 때만 렌더링되도록 하고, 페이지 번호 클릭 핸들러와 같은 함수를 아래와 같이 useCallback으로 감싸 불필요한 함수 재생성을 방지하는 방법이 효과적일 것 같다.

     const handlePage = useCallback((e: React.MouseEvent<HTMLElement>) => {
      const { id } = e.target as HTMLElement;
      setCurrentPageNum(Number(id));
    }, [setCurrentPageNum]);
    
    const handlePrevPage = useCallback(() => {
      if (currentPageNum <= 1) return;
      setCurrentPageNum(currentPageNum - 1);
    }, [currentPageNum, setCurrentPageNum]);
    
    const handleNextPage = useCallback(() => {
      if (currentPageNum >= totalPages) return;
      setCurrentPageNum(currentPageNum + 1);
    }, [currentPageNum, totalPages, setCurrentPageNum]);

💡 결론

현재 구현된 페이지네이션은 기본적인 기능을 충실히 수행하지만, 페이지가 많을 경우 사용자 경험이 제한적일 수 있다고 판단하였습니다. 위와 같은 변경 사항들을 통해 컴포넌트의 상태 관리가 보다 효율적으로 이루어지고, 불필요한 재렌더링을 방지하여 성능을 최적화할 수 있을 것이라고 생각합니다..!
마침 이번에 진행하는 프로젝트에서 페이지네이션이 필수 요구사항이라 이번 기회에 위 요구사항을 반영하여 페이지네이션을 새롭게 개발해보고자 합니다! 😯

profile
모든 익숙함에 물음표 더하기

0개의 댓글