게시글의 개수가 너무 많아졌을 때 페이징 처리를 하기 위한 Pagination을 구현해보자.
src/pages/Pagination.jsx
import React from 'react'
function Pagination() {
return (
<nav aria-label="Page navigation example">
<ul className="pagination justify-content-center">
<li className="page-item disabled">
<a className="page-link">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>
)
}
export default Pagination
Pagination
컴포넌트 불러오기getPosts
함수 수정src/pages/BlogList.jsx
...
import Pagination from './Pagination';
function BlogList({isAdmin}) {
...
const getPosts = (page = 1) => {
axios.get(`http://localhost:3001/posts`, {
params : {
_page: page,
_limit: 5,
_sort: 'id',
_order: 'desc'
}
})
.then((response) => {
setPosts(response.data);
// post 불러온 이후에 로딩중 상태 false로 변경
setLoading(false);
})
}
...
const renderBlogList = () => {
return posts.filter(post=> {
return isAdmin || post.publish
}).map(post => {
return (
<Card
key={post.id}
title={post.title}
onClick={() => navigate(`/blogs/${post.id}`)}>
{isAdmin ? <button
className="btn btn-danger btn-sm"
onClick={(event) => deleteBlog(event, post.id)}>
Delete</button> : null}
</Card>
)
})
}
return (
<div>
{renderBlogList()}
<Pagination />
</div>
)
}
...
renderBlogList
에서 게시글 5개의 경우에만 filter을 수행하기 때문에 이후에 publish = true
로 작성된 글이 있더라도 최초 다섯개 글 이후인 글은 출력되지 않는 오류 발생src/components/BlogList.jsx
renderBlogList
함수의 필터링 조건 지우기 const renderBlogList = () => {
return posts.map(post => {
return (
<Card
key={post.id}
title={post.title}
onClick={() => navigate(`/blogs/${post.id}`)}>
{isAdmin ? <button
className="btn btn-danger btn-sm"
onClick={(event) => deleteBlog(event, post.id)}>
Delete</button> : null}
</Card>
)
})
}
getPosts
함수에서 isAdmin이 false인 경우에는 publish : true
를 params로 전달하도록 수정const getPosts = (page = 1) => {
let params = {
_page: page,
_limit: 5,
_sort: 'id',
_order: 'desc',
}
if (!isAdmin) {
params = { ...params, publish: true }
}
axios.get(`http://localhost:3001/posts`, {
params : params
})
.then((response) => {
setPosts(response.data);
// post 불러온 이후에 로딩중 상태 false로 변경
setLoading(false);
})
}
currentPage
와 numberOfPages
를 props로 정의currentPage
는 active 된 페이지를 표시하기 위해 사용하는 numbernumberOfPages
는 page-item의 개수currentPage
는 기본 값을 1로, numberOfPages
는 필수 props로 지정src/componenets/Pagination.jsx
import React from 'react'
import propTypes from 'prop-types'
function Pagination({currentPage, numberOfPages}) {
return (
<nav aria-label="Page navigation example">
<ul className="pagination justify-content-center">
<li className="page-item disabled">
<a className="page-link">Previous</a>
</li>
{Array(numberOfPages).fill(1).map((value, index) => value + index)
.map((pageNumber) => {
return <li key = {pageNumber} className={`page-item ${currentPage === pageNumber ? 'active' : ''}`}>
<a className="page-link" href="#">{pageNumber}</a>
</li>
})}
<li className="page-item">
<a className="page-link" href="#">Next</a>
</li>
</ul>
</nav>
)
}
Pagination.propTypes = {
currentPage: propTypes.number,
numberOfPages: propTypes.number.isRequired
}
Pagination.defaultProps = {
currentPage: 1
}
export default Pagination
BlogList
에서 getPosts
함수를 Pagination
으로 props로 전달Pagination
컴포넌트에서는 전달 받은 함수를 pageNumber
를 클릭하면 pageNumber
를 전달하며 함수 실행src/componenets/Pagination.jsx
import React from 'react'
import propTypes from 'prop-types'
function Pagination({currentPage, numberOfPages, onClick}) {
return (
<nav aria-label="Page navigation example">
<ul className="pagination justify-content-center">
<li className="page-item disabled">
<a className="page-link">Previous</a>
</li>
{Array(numberOfPages).fill(1).map((value, index) => value + index)
.map((pageNumber) => {
return <li key = {pageNumber} className={`page-item ${currentPage === pageNumber ? 'active' : ''}`}>
<div className="page-link cursor-pointer"
onClick={() => {
onClick(pageNumber)
}}>
{pageNumber}
</div>
</li>
})}
<li className="page-item">
<a className="page-link" href="#">Next</a>
</li>
</ul>
</nav>
)
}
Pagination.propTypes = {
currentPage: propTypes.number,
numberOfPages: propTypes.number.isRequired,
onClick: propTypes.func.isRequired
}
Pagination.defaultProps = {
currentPage: 1
}
export default Pagination
Pagination
으로 전달할 currentPage
, numberOfPages
을 state로 정의currentPage
는 getPosts
함수를 실행할 때마다 해당 페이지에 해당하는 page number로 업데이트 하도록 수정numberOfPages
를 알기 위해서는 총 posts 수가 필요getPosts
함수에서 axios get 요청시 headers에 총 posts 함수가 포함되어 있음numberOfposts
를 state로 정의하고 get 요청시 numberOfposts
를 업데이트 하도록 수정numberOfPosts
가 변경될 때마다 numberOfPages
를 업데이트 하도록 useEffect
훅을 사용numberOfPages
가 1 이상일 때만 Pagination
컴포넌트를 출력하도록 수정src/components/BlogList.jsx
import React from 'react'
import axios from 'axios'
import { useState, useEffect } from 'react'
import Card from '../components/Card';
import LoadingSpinner from '../components/LoadingSpinner';
import Pagination from './Pagination';
import { Link, useNavigate } from 'react-router-dom';
import { bool } from 'prop-types';
function BlogList({isAdmin}) {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
// ********** state 정의
const [currentPage, setCurrentPage] = useState(1);
const [numberOfPosts, setNumberOfPosts] = useState(0);
const [numberOfPages, setNumberOfPages] = useState(0);
// **** numberOfPages 업데이트
const limit = 5; // 한 페이지당 보여줄 글 수
useEffect(() => {
setNumberOfPages(Math.ceil(numberOfPosts / limit))
}, [numberOfPosts])
const getPosts = (page = 1) => {
// ***** currentPage 업데이트
setCurrentPage(page)
let params = {
_page: page,
_limit: 5,
_sort: 'id',
_order: 'desc',
}
if (!isAdmin) {
params = { ...params, publish: true }
}
axios.get(`http://localhost:3001/posts`, {
params : params
})
.then((response) => {
// ****** post 개수 업데이트
setNumberOfPosts(response.headers['x-total-count'])
setPosts(response.data);
// post 불러온 이후에 로딩중 상태 false로 변경
setLoading(false);
})
}
...
return (
<div>
{renderBlogList()}
{ numberOfPages > 1 && <Pagination currentPage={currentPage}
numberOfPages={numberOfPages}
onClick={getPosts} />}
</div>
)
}
BlogList.propTypes = {
isAdmin: bool
}
BlogList.defaultProps = {
isAdmin: false
}
export default BlogList
src/components/Pagination.jsx
import React from 'react'
import propTypes from 'prop-types'
function Pagination({currentPage, numberOfPages, onClick, limit}) {
// startPage 계산
const currentSet = Math.ceil(currentPage / limit);
const startPage = limit * (currentSet - 1) + 1;
// pagination 배열 크기 계산
const lastSet = Math.ceil(numberOfPages/limit);
const numberOfPagesForSet = currentSet === lastSet ? numberOfPages%limit : limit
return (
<nav aria-label="Page navigation example">
<ul className="pagination justify-content-center">
{/* 첫번째 세트에서는 Previous 안보이게 설정 */}
{ currentSet !== 1 && <li className="page-item">
<div className="page-link cursor-pointer"
onClick={() => onClick(startPage - limit)}>Previous</div>
</li>}
{Array(numberOfPagesForSet).fill(startPage)
.map((value, index) => value + index)
.map((pageNumber) => {
return <li key = {pageNumber} className={`page-item ${currentPage === pageNumber ? 'active' : ''}`}>
<div className="page-link cursor-pointer"
onClick={() => {
onClick(pageNumber)
}}>
{pageNumber}
</div>
</li>
})}
{/* 마지막 세트에서는 Next 버튼 안보이게 설정 */}
{ currentSet !== lastSet && <li className="page-item">
<div className="page-link cursor-pointer"
onClick={() => onClick(startPage + limit)}
>Next</div>
</li>}
</ul>
</nav>
)
}
Pagination.propTypes = {
currentPage: propTypes.number,
numberOfPages: propTypes.number.isRequired,
onClick: propTypes.func.isRequired,
limit: propTypes.number
}
Pagination.defaultProps = {
currentPage: 1,
limit: 5
}
export default Pagination
onClickPageButton
함수 작성getPosts
함수 포함Pagination
onClick 함수에 작성useLocation
훅 사용pageParam
이 변경될 때마다 currentPage
업데이트, getPosts
실행...
import { useNavigate, useLocation } from 'react-router-dom';
function BlogList({isAdmin}) {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
const location = useLocation();
const params = new URLSearchParams(location.search);
const pageParam = params.get('page');
...
const onClickPageButton = (page) => {
navigate(`${location.pathname}?page=${page}`)
getPosts(page)
}
const getPosts = (page = 1) => {
let params = {
_page: page,
_limit: 5,
_sort: 'id',
_order: 'desc',
}
if (!isAdmin) {
params = { ...params, publish: true }
}
axios.get(`http://localhost:3001/posts`, {
params : params
})
.then((response) => {
// post 개수
setNumberOfPosts(response.headers['x-total-count'])
setPosts(response.data);
// post 불러온 이후에 로딩중 상태 false로 변경
setLoading(false);
})
}
useEffect(() => {
setCurrentPage(parseInt(pageParam) || 1)
getPosts(parseInt(pageParam) || 1)
}, [pageParam])
...
return (
<div>
{renderBlogList()}
{ numberOfPages > 1 && <Pagination currentPage={currentPage}
numberOfPages={numberOfPages}
onClick={onClickPageButton} />}
</div>
)
}
BlogList.propTypes = {
isAdmin: bool
}
BlogList.defaultProps = {
isAdmin: false
}
export default BlogList