[React : mini-project] Pagination (json-server)

문지은·2023년 8월 5일
0

React

목록 보기
21/24
post-thumbnail
post-custom-banner

게시글의 개수가 너무 많아졌을 때 페이징 처리를 하기 위한 Pagination을 구현해보자.

Pagination 컴포넌트 만들기

  • bootstrap pagination을 사용하여 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 컴포넌트 불러오기
    • axios 요청시 한 페이지당 5개 글을 id 기준으로 내림차순 출력하도록 getPosts 함수 수정
    • 요청 방식은 json-server 공식문서 참고

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로 작성된 글이 있더라도 최초 다섯개 글 이후인 글은 출력되지 않는 오류 발생
    • isAdmin / publish 값에 따른 post 필터링을 front 에서 하는 것이 아니라 back 에서 하도록 수정

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);
    })
  }
  • 이제 ListPage에서도 정상 출력됨을 확인

props 정의

  • currentPagenumberOfPages를 props로 정의
    • currentPage는 active 된 페이지를 표시하기 위해 사용하는 number
    • numberOfPages는 page-item의 개수
    • currentPage 는 기본 값을 1로, numberOfPages는 필수 props로 지정
  • JavaScript의 map 함수를 사용하여 numberOfPages 만큼 page-item을 출력하도록 작성

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

onClick 함수 정의

  • page number를 클릭하면 해당 페이지의 post를 출력할 수 있도록 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

props 전달

  • Pagination으로 전달할 currentPage, numberOfPages을 state로 정의
  • currentPagegetPosts 함수를 실행할 때마다 해당 페이지에 해당하는 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

Next, Previous 버튼 활성화

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
  • 페이지 이동 후 뒤로가기를 눌렀을때 이전에 본 페이지를 확인할 수 있게 하기 위해 navigate를 이용하여 경로 기록에 포함시키기
    • onClickPageButton 함수 작성
      • page에 따른 navigate
      • getPosts 함수 포함
      • Pagination onClick 함수에 작성
  • params 정보 얻기 위해 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


profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈
post-custom-banner

0개의 댓글