본문 바로가기
React

React 페이지네이션 UI 직접 만들기

by Vintz 2023. 8. 2.
반응형

페이지네이션(pagination)은 큰 데이터를 사용자가 보기 편하도록 작은 부분으로 나누어 여러 페이지로 표시하는 것을 뜻한다. 사용자는 이 페이지들을 순차적으로 또는 선택적으로 볼 수 있다. 요즘은 비즈니스나 UX적인 측면에서 무한 스크롤이 더 많이 쓰이는 것 같지만, 사용자가 특정 정보를 찾기 위해 몇 페이지를 건너뛰어야 하거나, 어드민의 데이터 테이블 UI와 같이 내용이 많고 구조화된 정보를 제공해야 하는 경우에는 페이지네이션이 더 적합할 수 있다.

페이지네이션 핵심 로직 이해하기

페이지네이션의 일반적인 예를 들자면, 사용자는 페이지마다 보여지는 데이터의 양을 선택할 수 있고, 원하는 페이지 번호를 클릭함으로써 해당 페이지로 즉시 이동할 수 있다. 또한, 앞뒤 페이지로 쉽게 이동할 수 있는 네비게이션 버튼이 존재한다. 여기서 페이지네이션의 핵심 구성 요소인 전체 페이지의 수와 현재 페이지에 표시될 데이터 목록을 계산할 줄 알면, 쉽게 구현할 수 있다.

 

전체 페이지의 수는 전체 데이터 수를 한 페이지에 표시될 데이터 수로 나누어 계산한다. 이때, 계산 결과에 소수점이 있을 수 있기 때문에 항상 올림하여 계산해야 한다. 예를 들어, 28개의 데이터가 있고, 한 페이지에 10개의 데이터를 표시하는 경우를 생각해보자. 여기서 전체 페이지 수는 28을 10으로 나눈 2.8을 올림하여 총 3페이지가 된다.

 

다음으로, 현재 페이지에 표시될 데이터 목록을 보여주기 위해서는 첫 번째 인덱스와 마지막 인덱스를 계산하고, 해당 범위의 데이터를 잘라내야 한다. 이를 위해 배열의 slice() 메서드를 사용할 수 있다.

 

첫 번째 인덱스와 마지막 인덱스를 계산하는 방법은 다음과 같다:

  • 첫 번째 인덱스: (현재 페이지 - 1) * 한 페이지에 표시될 데이터 수
  • 마지막 인덱스: 첫 번째 인덱스 + 한 페이지에 표시될 데이터 수

가령, 전체 데이터가 28개이고 한 페이지에 표시할 데이터가 10개인 경우를 생각해보자. 만약 현재 페이지가 2페이지라면 첫 번째 인덱스는 (2 - 1) * 10 = 10, 마지막 인덱스는 10 + 10 = 20이 된다.

게시물 목록의 페이지네이션 구현하기

전체 코드는 CodeSandBox에서 확인하실 수 있습니다.

위의 핵심 로직 내용을 바탕으로 게시물 목록에 페이지네이션을 적용해보자. 이 구현에서는 UI를 다음과 같은 컴포넌트들로 구성했다.

그리고 컴포넌트 계층 구조는 다음과 같다:

  • FilterablePostList
    • PostList
    • DisplayCountSelector
    • Pagination

FilterablePostList

공통 부모 컴포넌트에 필요한 최소한의 state를 정의해보자. 그리고 게시물 목록은 무료 API인 JSON Placeholder를 통해서 불러오며, useEffect 훅을 사용하여 이러한 외부 데이터를 posts 변수에 동기화를 시켜준다.

 

나머지 필요한 값들, 즉 첫 번째 인덱스, 마지막 인덱스, 그리고 현재 페이지에 표시되어야 할 게시물들을 계산해준다.

import { useEffect, useState } from "react";

import { DisplayCountSelector } from "./DisplayCountSelector";
import { PostList } from "./PostList";
import { Pagination } from "./Pagination";

export function FilterablePostList() {
  const [posts, setPosts] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [postsPerPage, setPostsPerPage] = useState(10);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((data) => setPosts(data));
  }, []);

  const firstPostIndex = (currentPage - 1) * postsPerPage;
  const lastPostIndex = firstPostIndex + postsPerPage;
  const currentPosts = posts.slice(firstPostIndex, lastPostIndex);

  return (
    <>
      <header>
        <h1>게시물 목록</h1>
      </header>

      <main>
        <DisplayCountSelector
          setPostsPerPage={setPostsPerPage}
          setCurrentPage={setCurrentPage}
        />
        <PostList list={currentPosts} />
      </main>

      <footer>
        <Pagination
          postsNum={posts.length}
          postsPerPage={postsPerPage}
          setCurrentPage={setCurrentPage}
          currentPage={currentPage}
        />
      </footer>
    </>
  );
}

DisplayCountSelector

페이지당 게시물 수를 선택할 수 있는 이 컴포넌트는 setPostsPerPagesetCurrentPage라는 두 개의 set(설정자) 함수 prop을 받는다. onChange 속성을 보면 함수가 연결되어 있다. 해당 함수는 값이 변경될 때마다 호출되며, 그 값은 사용자가 선택한 페이지당 게시물의 수이다.

 

해당 함수는 두 가지 일을 수행한다:

  1. 현재 페이지를 1로 설정
  2. 페이지당 게시물 수를 변경

첫 번째로, 현재 페이지를 초기화 해준다. 이렇게 해주는 게 UX적인 면도 있지만, 초기화 해주지 않으면 인덱스가 전체 게시물의 수를 넘을 수도 있다. 예를 들어, 만약 페이지당 게시물 수가 24이고 현재 페이지가 5인 상황에서 페이지당 게시물 수를 50으로 변경하면, 첫 번째 인덱스는 (5 - 1) * 50 = 200이며, 마지막 인덱스는 200 + 50 = 250이 된다. 따라서 currentPosts는 첫 번째 인덱스(200)가 posts 배열의 길이(100)보다 크기 때문에, 빈 배열을 반환하게 된다.

 

두 번째로, 페이지당 게시물 수를 변경한다. 여기서 주의할 점은, 사용자가 선택한 값이 문자열로 전달되므로 이를 Number 함수를 사용하여 숫자로 변환한다는 점이다.

export function DisplayCountSelector({ setPostsPerPage, setCurrentPage }) {
  return (
    <div>
      <label htmlFor="displayCount">페이지당 게시물 수</label>

      <select
        id="displayCount"
        onChange={({ target: { value } }) => {
          setCurrentPage(1);
          setPostsPerPage(Number(value));
        }}
      >
        <option value="10">10</option>
        <option value="16">16</option>
        <option value="24">24</option>
        <option value="30">30</option>
        <option value="50">50</option>
        <option value="100">100</option>
      </select>
    </div>
  );
}

PostList

게시물 목록은 간단하다. 현재 페이지에 대한 게시물 목록이 저장되어 있는 list 배열을 prop으로 받아서 화면에 표시해준다.

export function PostList({ list }) {
  return (
    <ol>
      {list.map(({ id, title, body }) => (
        <li key={id}>
          <h2>
            {id}. {title}
          </h2>
          <p>{body}</p>
        </li>
      ))}
    </ol>
  );
}

Pagination

마지막으로, Pagination 컴포넌트를 살펴보자. 하는 일이 많은 것 같지만 별거 없다. 이 컴포넌트는 네 개의 prop을 받는다: postsNum, postsPerPage, setCurrentPage, currentPage.

 

먼저, totalPages는 전체 페이지의 수를 계산한다. Math.ceil 메서드를 사용하여 전체 게시물 수를 페이지당 게시물 수로 나누었을 때 올림 처리를 해주어 계산한다. 그 후, for 문을 사용하여 각 페이지 번호를 pageList에 담는다.

 

다음으로, 다음 페이지와 이전 페이지로 이동하는 함수를 만들어주었다. 그리고 만약 전체 페이지 수가 1이라면, 페이지네이션을 표시할 필요가 없으므로 null을 반환한다.

 

각 페이지 번호 버튼들은 onClick 이벤트에 함수를 연결하여 클릭 시 해당 페이지로 이동하게 된다.

export function Pagination({
  postsNum,
  postsPerPage,
  setCurrentPage,
  currentPage
}) {
  const pageList = [];
  const totalPages = Math.ceil(postsNum / postsPerPage);

  for (let i = 1; i <= totalPages; i++) {
    pageList.push(i);
  }

  const goToNextPage = () => {
    setCurrentPage(currentPage + 1);
  };

  const goToPrevPage = () => {
    setCurrentPage(currentPage - 1);
  };

  if (totalPages === 1) {
    return null;
  }

  return (
    <div>
      <button onClick={goToPrevPage} disabled={currentPage === 1}>
        prev
      </button>

      {pageList.map((page) => (
        <button
          key={page}
          onClick={() => setCurrentPage(page)}
          className={currentPage === page ? "active" : ""}
        >
          {page}
        </button>
      ))}

      <button onClick={goToNextPage} disabled={currentPage === pageList.length}>
        next
      </button>
    </div>
  );
}

실행해보기

마치며

리액트로 페이지네이션을 구현하는 방법에 대해 알아보았다. 이전에는 좋은 라이브러리를 활용하면 모든 UI 문제가 해결될 것이라고 생각했다. 그 이유는 이미 많은 라이브러리들이 존재하고, 특히 리액트의 커뮤니티는 굉장히 커서 필요한 대부분의 것들을 찾을 수 있었기 때문이다. 하지만 실무에서는 적합한 라이브러리를 찾아보고, 문서를 읽고, 커스텀이 가능한지 확인하는 것보다 때로는 그냥 직접 구현하는 것이 더 나을 때가 있다는 것을 깨달았다.

 

이런 것들은 많이 해봐야 실력이 느는 것 같다. 처음에는 이해하기 어렵고 복잡하게 느껴졌던 것들이지만, 이 글을 쓰면서 컴포넌트 하나하나를 유심히 보았더니, 이제는 페이지네이션을 스스로 구현하는 것이 어렵지 않을 것 같다.

참고

React로 페이지네이션 UI 구현하기 - DaleSeo
React Pagination in 7 minutes [ EASY ] | Pagination Tutorial - CodeBlessYou(유튜브 채널)
반응형