리알못 React: 3. 페이지 매기기(Pagination)

Table of Content

이 글은 Code with Mosh의 Mastering React 과정을 공부하며 정리한 글입니다. 리알못이라 이상하거나 틀린 내용이 있을 것입니다…

사전 준비

Bootstrap 설치

npm i bootstrap@4.1.1 명령어로 React 프로젝트에 Bootstrap을 설치한 후 index.js 파일에 import 'bootstrap/dist/css/bootstrap.css';를 추가해줍니다.

lodash 설치

lodash는 자바스크립트 유틸리티 라이브러리입니다. 여기서는 배열을 편집하기 위해 사용합니다. npm i lodash@4.17.10 명령어로 React 프로젝트에 lodash를 설치한 후 lodash를 사용하려는 컴포넌트에서 import _ from 'lodash'; 를 추가해줍니다.

Bootstrap을 이용한 간단한 페이지 수 매기기

Bootstrap Pagination 문서를 보면 Pagination 예제를 볼 수 있습니다. 아래는 화면 하단에 간단하게 페이지 수를 매긴 예제입니다.

/***** App.js *****/

import React from 'react';

const App = () => {
  return (
    <React.Fragment>
      <div>안녕요?ㅎ</div>

          {/* Pagination */}
      <nav aria-label="Page navigation example">
        <ul className="pagination">
          <li className="page-item"><a className="page-link" href="#">Previous</a></li>
          <li className="page-item"><a className="page-link" href="#">1</a></li>
          <li className="page-item"><a className="page-link" href="#">2</a></li>
          <li className="page-item"><a className="page-link" href="#">3</a></li>
          <li className="page-item"><a className="page-link" href="#">Next</a></li>
        </ul>
      </nav>
    </React.Fragment>
  );
};

export default App;

아이템 수에 비례하여 페이지 수 표시하기

위의 예제는 정적으로 페이지 수를 매긴 예제입니다. 실제로는 페이지에 보여 줄 아이템 수가 변할 수 있기 때문에 동적으로 페이지 수를 매겨야 합니다. 예를 들어 아이템이 10개가 있고 각 페이지에 보여줄 아이템 수를 3개로 정한다면 3 + 3 + 3 + 1 총 4페이지가 필요하지요.

아래 예제는 페이지에 보여 줄 아이템 수와 한 페이지엡 보여 줄 아이템 수에 따라 하단에 페이지 수를 매기는 예제입니다.

/***** App.js *****/

import React from 'react';
import MoviesPage from './components/MoviesPage';

const App = () => {
  return <MoviesPage />;
};

export default App;
/***** ./components/MoviesPage.js *****/

import React, { useState } from 'react';
import Pagination from './common/Pagination';

const MoviesPage = () => {
  const getMovies = () => { // 영화 정보를 반환하는 함수
    const movies = [
      { id: 0, title: "기생충", genre: "블랙 코미디", release: "2019-05-30" },
      { id: 1, title: "라이온 킹", genre: "애니메이션", release: "2019-07-17" },
      { id: 2, title: "날씨의 아이", genre: "애니메이션", release: "2019-10-31" },
      { id: 3, title: "알라딘", genre: "판타지", release: "2019-05-23" },
      { id: 4, title: "나랏말싸미", genre: "역사", release: "2019-07-24" },
      { id: 5, title: "주전장", genre: "역사", release: "2019-07-25" },
      { id: 6, title: "어벤져스: 엔드게임", genre: "판타지", release: "2019-04-24" },
      { id: 7, title: "봉오동 전투", genre: "역사", release: "2019-08-07" },
      { id: 8, title: "김복동", genre: "역사", release: "2019-08-08" },
      { id: 9, title: "코코", genre: "애니메이션", release: "2018-01-11" },
    ]
    return movies;
  }

  const [movies, setMovies] = useState({ // 영화 정보를 담는 state
    data: getMovies(),
    pageSize: 3 // 한 페이지에 보여줄 아이템(영화목록) 개수
  });

  const { length: count } = movies.data;
  if(count === 0)
    return <p>영화 정보가 없습니다.</p>

  return (
    <>
      <p>{count} 개의 영화 정보가 있습니다.</p>

      <table className="table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Genre</th>
            <th>Release</th>
          </tr>
        </thead>
        <tbody>
          {movies.data.map(movie =>
            <tr key={movie.id}>
              <td>{movie.id}</td>
              <td>{movie.title}</td>
              <td>{movie.genre}</td>
              <td>{movie.release}</td>
            </tr>
          )}
        </tbody>
      </table>

      <Pagination
        itemsCount={count}
        pageSize={movies.pageSize}
      />
    </>
  );
};

export default MoviesPage;
/***** ./components/common/Pagination.js *****/

import React from 'react';
import _ from 'lodash';

const Pagination = (props) => {
  const { itemsCount, pageSize } = props; // 각각 아이템(영화목록) 개수, 한 페이지에 보여줄 아이템(영화목록) 개수
  const pageCount = Math.ceil(itemsCount / pageSize); // 몇 페이지가 필요한지 계산

  if (pageCount === 1) return null; // 1페이지 뿐이라면 페이지 수를 보여주지 않음

  const pages = _.range(1, pageCount + 1); // 마지막 페이지에 보여줄 컨텐츠를 위해 +1, https://lodash.com/docs/#range 참고

  return (
    <nav> {/* VSCode 입력: nav>ul.pagination>li.page-item>a.page-link */}
      <ul className="pagination">
        {pages.map(page => (
          <li key={page} className="page-item" style={{ cursor: "pointer" }}>
            <a className="page-link">{page}</a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

export default Pagination;

페이지 변경 이벤트 처리

페이지를 매긴 후엔 현재 페이지 위치를 시작적으로 보여주고 페이지 수를 클릭했을 때 발생한 이벤트를 처리해야 합니다. 아래 예제는 그런 예제입니다. 간단하게 보여주기 위해 위 예제에서 사용한 아이템 추가 코드를 삭제하고 아이템 개수 State로 대체했습니다.

/***** ./components/MoviesPage.js *****/

import React, { useState } from 'react';
import Pagination from './common/Pagination';

const MoviesPage = () => {
  const [movies, setMovies] = useState({ // 영화 정보를 담는 state
    data: "", // 생략...
    pageSize: 2, // 한 페이지에 보여줄 아이템(영화목록) 개수
    itemsCount: 10, // 아이템(영화 정보) 개수 (임의로 10으로 설정함)
    currentPage: 1 // 현재 활성화된 페이지 위치
  });

  const handlePageChange = (page) => {
    setMovies({ ...movies, currentPage: page });
  }

  const { pageSize, itemsCount, currentPage } = movies;

  return (
    <>
      <p>(대충 페이지 내용이 들어갈 부분)</p>
      <Pagination 
        pageSize={pageSize} 
        itemsCount={itemsCount} 
        currentPage={currentPage} 
        onPageChange={handlePageChange}
      />
    </>
  );
};

export default MoviesPage;
/***** ./components/common/Pagination.js *****/

import React from 'react';
import _ from 'lodash';

const Pagination = (props) => {
  const { itemsCount, pageSize, currentPage, onPageChange } = props; // 각각 아이템(영화목록) 개수, 한 페이지에 보여줄 아이템(영화목록) 개수

  const pageCount = Math.ceil(itemsCount / pageSize); // 몇 페이지가 필요한지 계산
  if (pageCount === 1) return null; // 1페이지 뿐이라면 페이지 수를 보여주지 않음

  const pages = _.range(1, pageCount + 1); // 마지막 페이지에 보여줄 컨텐츠를 위해 +1, https://lodash.com/docs/#range 참고

  return (
    <nav> {/* VSCode 입력: nav>ul.pagination>li.page-item>a.page-link */}
      <ul className="pagination">
        {pages.map(page => (
          <li 
            key={page} 
            className={page === currentPage ? "page-item active" : "page-item"} // Bootstrap을 이용하여 현재 페이지를 시각적으로 표시
            style={{ cursor: "pointer" }}>
              <a className="page-link" onClick={() => onPageChange(page)}>{page}</a> {/* 페이지 번호 클릭 이벤트 처리기 지정 */}
          </li>
        ))}
      </ul>
    </nav>
  );
}

export default Pagination;
  • 출력화면

페이지 별로 아이템 보여주기

지금까지의 예제는 어떤 페이지 수를 클릭해도 모든 아이템을 보여줬습니다. 실제로는 페이지 별로 다른 아이템을 보여줘야 합니다. 이를 위해 lodash를 사용하여 배열을 잘라 각 페이지 별로 아이템이 속한 배열을 얻어옵니다.

/***** ../utils/paginate.js *****/

import _ from 'lodash';

export function paginate(items, pageNumber, pageSize) {
  const startIndex = (pageNumber - 1) * pageSize; // 자를 배열의 시작점

  return _(items)
    .slice(startIndex) // 시작점부터 배열을 자르되
    .take(pageSize) // pageSize만큼의 배열을 취함
    .value(); // lodash wrapper 객체를 regular 배열로 변환
}
/***** ./components/MoviesPage.jsx *****/

import React, { useState } from 'react';
import Pagination from './common/Pagination';
import { paginate } from '../utils/paginate';

const MoviesPage = () => {
  const getMovies = () => {
    const movies = [
      { id: 0, title: "기생충", genre: "블랙 코미디", release: "2019-05-30" },
      { id: 1, title: "라이온 킹", genre: "애니메이션", release: "2019-07-17" },
      { id: 2, title: "날씨의 아이", genre: "애니메이션", release: "2019-10-31" },
      { id: 3, title: "알라딘", genre: "판타지", release: "2019-05-23" },
      { id: 4, title: "나랏말싸미", genre: "역사", release: "2019-07-24" },
      { id: 5, title: "주전장", genre: "역사", release: "2019-07-25" },
      { id: 6, title: "어벤져스: 엔드게임", genre: "판타지", release: "2019-04-24" },
      { id: 7, title: "봉오동 전투", genre: "역사", release: "2019-08-07" },
      { id: 8, title: "김복동", genre: "역사", release: "2019-08-08" },
      { id: 9, title: "코코", genre: "애니메이션", release: "2018-01-11" },
    ]
    return movies;
  }

  const [movies, setMovies] = useState({ // 영화 정보를 담는 state
    data: getMovies(), // 영화 정보
    pageSize: 3, // 한 페이지에 보여줄 아이템(영화목록) 개수
    currentPage: 1 // 현재 활성화된 페이지 위치
  });

  const handlePageChange = (page) => {
    setMovies({ ...movies, currentPage: page });
  };

  const { data, pageSize, currentPage } = movies;
  const pagedMovies = paginate(data, currentPage, pageSize); // 페이지 별로 아이템이 속한 배열을 얻어옴

  const { length: count } = movies.data;
  if (count === 0) 
    return <p>영화 정보가 없습니다.</p>;

  return (
    <>
      <p>{count} 개의 영화 정보가 있습니다.</p>

      <table className="table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Genre</th>
            <th>Release</th>
          </tr>
        </thead>
        <tbody>
          {pagedMovies.map((movie) => (
            <tr key={movie.id}>
              <td>{movie.id}</td>
              <td>{movie.title}</td>
              <td>{movie.genre}</td>
              <td>{movie.release}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <Pagination
        pageSize={pageSize}
        itemsCount={count}
        currentPage={currentPage}
        onPageChange={handlePageChange}
      />
    </>
  );
};

export default MoviesPage;
  • ./components/common/Pagination.jsx
import React from 'react';
import _ from 'lodash';

const Pagination = (props) => {
  const { itemsCount, pageSize, currentPage, onPageChange } = props; // 각각 아이템(영화목록) 개수, 한 페이지에 보여줄 아이템(영화목록) 개수

  const pageCount = Math.ceil(itemsCount / pageSize); // 몇 페이지가 필요한지 계산
  if (pageCount === 1) return null; // 1페이지 뿐이라면 페이지 수를 보여주지 않음

  const pages = _.range(1, pageCount + 1); // 마지막 페이지에 보여줄 컨텐츠를 위해 +1, https://lodash.com/docs/#range 참고

  return (
    <nav> {/* VSCode 입력: nav>ul.pagination>li.page-item>a.page-link */}
      <ul className="pagination">
        {pages.map(page => (
          <li 
            key={page} 
            className={page === currentPage ? "page-item active" : "page-item"} // Bootstrap을 이용하여 현재 페이지를 시각적으로 표시
            style={{ cursor: "pointer" }}>
              <a className="page-link" onClick={() => onPageChange(page)}>{page}</a> {/* 페이지 번호 클릭 이벤트 처리기 지정 */}
          </li>
        ))}
      </ul>
    </nav>
  );
}

export default Pagination;
  • 실행화면

타입 체크(Type Checking)

props에 엉뚱한 값을 전달하면 에러가 발생하지 않지만 엉뚱한 결과가 표시될 수 있습니다. 예를 들어 숫자를 전달해야 하지만 문자열을 넘기는 경우 콘솔에 경고나 에러가 발생하지 않을 수 있습니다. 이를 막거나 버그를 잡기 쉽도록 타입 체크가 필요합니다.

이를 위해 PropTypes라는 라이브러리를 사용합니다. 이 라이브러리는 리액트 버전 15 이후 별도로 설치하여 사용해야 합니다. 설치 명령어는 npm i prop-types@15.6.2이며, 사용하려는 컴포넌트에서 import PropTypes from 'prop-types'; 코드로 import 후 사용하면 됩니다.

PropTypes에 대한 구체적인 사용 법은 https://reactjs.org/docs/typechecking-with-proptypes.html 을 참고하시기 바랍니다. PropTypes 라이브러리를 사용함으로써 render() 함수 내에 여기저기 사용된 props를 찾아보지 않고 별도로 코딩한 PropTypes 부분만 보면 되므로 좀 더 편할 수 있습니다.

/***** App.js *****/

import React from 'react';
import MyComponent from './components/MyComponent';

const App = () => {
  return <MyComponent title="안녕요?ㅎ" year={2020} />;
};

export default App;
/***** ./components/myComponent.js *****/

import React from 'react';
import PropTypes from 'prop-types'; // PropTypes import

const MyComponent = (props) => {
  const { title, year } = props;

  return (
    <React.Fragment>
      <h1>{title}</h1>
      <p>올해는 {year}년입니다.</p>
    </React.Fragment>
  );
}

// 타입 체크
MyComponent.propTypes = {
  title: PropTypes.string.isRequired,
  year: PropTypes.number.isRequired
}

export default MyComponent;

위 코드 중 App.js에서 year를 {2019}가 아닌 "2019"로 지정하면 숫자가 아닌 문자열을 넘기게 됩니다. MyComponent.jsx에서 year의 타입은 number임을 명시했으므로 콘솔에 다음과 같은 경고가 뜨게 됩니다. 마찬가지로 title과 year는 isRequired이므로 값을 지정하지 않으면 콘솔에 경고가 발생하게 됩니다.

“리알못 React: 3. 페이지 매기기(Pagination)” 에 대한 2 댓글

  1. 리액트로 개발하면서 페이징처리하고 있는데 많은 도움이 되었습니다. 정말 감사합니다.

댓글 남기기