Node.js & mongoose 페이징 기능 구현하기

gwjeon·2020년 3월 6일
1
post-custom-banner


✅ 완성 모습

이 페이지는 maxPost, maxPage 5로 구성 된 페이지이다. 아래 설명은 10으로 가정한다.
https://my-first-board.herokuapp.com/


✅ 학습 목표

  • 페이징 기능을 구현할 수 있다.
  • 내부적으로 사용되는 변수의 기능을 이해할 수 있다.
  • noSQL 방식에서의 페이징 기법을 이해할 수 있다.

✅ 사용 기술

  • Node.js
  • Mongoose
  • Pug

✅ 구현 하기

✔ 컨트롤러 만들기

export const pagingController = async (req, res) => {
  const { page } = req.query; // (1)
  try {
    const totalPost = await Post.countDocuments({}); // (2)
    if (!totalPost) { // (3)
      throw Error();
    }
    let {
      startPage,
      endPage,
      hidePost,
      maxPost,
      totalPage,
      currentPage
    } = paging(page, totalPost); // (4)
    const board = await Post.find({}) // (5)
      .sort({ createAt: -1 })
      .skip(hidePost)
      .limit(maxPost);
    res.render("home", { // (6)
      board,
      currentPage,
      startPage,
      endPage,
      maxPost,
      totalPage,
    });
  } catch (error) {
    res.render("home", { board: [] }); // (7)
  } 
};

✔ 설명

  • (1) : req.query 객체를 통해서 URL의 쿼리스트링을 가져옴. ex) http://페이징구현/?page=1
  • (2) : 현재 DB에 존재하는 모든 Post(게시물)의 총 개수를 가져옴.
  • (3) : 만약 총 개시물이 없을 경우 Error를 던져 (7)번으로 분기함.
  • (4) : paging에 필요한 변수들을 만들기 위해 page와 totalPost를 아규먼트로 함수 호출하여 ES6 비구조화 할당으로 리턴 받는다.
  • (5) : db에서 (4)에서 받아온 변수를 토대로 필요한 게시물들을 받아오자. 여기서 mongoose의 sort와 skip, limit를 쓰고 있는데 sort는 내림차순으로 정렬하겠다는 것이고, skip은 제외할 게시물, limit은 제외할 게시물을 제외하고 그 다음부터 찾을 게시물이다. 즉 hidePost가 20이고 limit가 5라면 21~25 게시물을 받아온다.
  • (6) : 받아온 db 데이터와 화면에서 렌더링 하기 위해 필요한 변수들을 템플릿으로 전달한다.
  • (7) : (3)번에서 에러로 분기 했을 경우는 게시물이 없는 경우이다. 빈 board 객체를 템플릿으로 전달한다.

✔ paging 함수

const paging = (page, totalPost) => {
  const maxPost = 10; // (1)
  const maxPage = 10; // (2)
  let currentPage = page ? parseInt(page) : 1; // (3)
  const hidePost = page === 1 ? 0 : (page - 1) * maxPost; // (4)
  const totalPage = Math.ceil(totalPost / maxPost); // (5)
  
  if (currentPage > totalPage) { // (6)
    currentPage = totalPage;
  }

  const startPage = Math.floor(((currentPage - 1) / maxPage)) * maxPage + 1; // (7)
  let endPage = startPage + maxPage - 1; // (8)

  if (endPage > totalPage) { // (9)
    endPage = totalPage;
  }

  return { startPage, endPage, hidePost, maxPost, totalPage, currentPage }; // (10)
};

export default paging;
  • (1) maxPost : 한 페이지에 표시 하고자하는 최대 게시물의 수
  • (2) maxPage : 한 페이지에 표시 하고자하는 최대 페이지의 수
  • (3) currentPage : 현재 페이지, 만약 쿼리스트링으로 현재 페이지(page)를 받아 왔을 경우 parseInt 메서드를 통해 정수화하고 아닐 경우 1
  • (4) hidePost : DB에서 게시물을 불러올때 제외하고 불러올 게시물의 수 만약 page가 1일 경우 제외 할 게시물이 없음으로 0, 있을 경우 -1을 하여 maxPost를 곱해준다. -1을 하는 이유는 예를 들어 3페이지를 출력 할 경우 1페이지는 1~10의 게시물, 2페이지는 11~20의 게시물, 3페이지는 21~30의 게시물을 출력 해야하는데 -1을 해주지 않으면 31부터 40의 게시물을 skip 하게 된다.
  • (5) totalPage : 총 페이징 해야 할 페이지의 수 totalPost(DB에 존재하는 게시물의 수) / maxPost를 하면 간단히 구할 수 있다. Math.ceil 메서드를 통해 나머지가 생길 경우를 대비한다. Math.ceil 메서드는 나머지를 무시하고 올림한다. floor을 쓰지 않고 ceil을 쓰는 이유는 만약 floor을 썼을 경우 totalPost가 33이면 33 / 5는 6이 된다. 33개의 게시물을 표시 하기 위해선 총 7개의 페이지가 필요하다.
  • (6) : 화면 UI를 통하지 않고 URL에서 직접 접근 하는 경우가 생길 수 있다. 그럴 경우를 대비해서 currentPage(현재 페이지)가 totalPage(총 페이지)보다 클 경우 currentPage를 마지막 페이지(totalPage의 개수가 마지막 페이지가 된다)로 바꾸어 준다.
  • (7) startPage : 화면에 표시할 페이지의 시작 번호를 구한다. 예를 들어 현재 3페이지일 경우 startPage는 1이 나와야 한다. currentPage에서 -1을 빼주고 maxPage와 나눈다. -1을 하는 이유는 만약 현재 페이지가 20페이지일 경우 startPage는 11이 되어야 한다. -1을 해주지 않으면 startPage가 21페이지가 되어버린다. 그러고나서 maxPage를 곱해주고 + 1을 해준다.
  • (8) endPage : 화면에 표시할 페이지의 마지막 번호를 구한다. startPage는 이시점에서 이미 결정이 되어 있기 때문에 그냥 maxPage를 더한 다음 -1 해주면 된다. 만약 startPage가 11 일경우 endPage는 20 이여야 한다. 11 + 10 - 1 하면 20이 된다.
  • (9) : endPage가 totalPage보다 큰 경우 예를 들어 totalPage의 개수는 36일때 startPage는 31일 것이다. 이때 endPage는 40이 될터인데 총 page는 36개이면 충분하다. 이것을 방지하기 위하여 치환 해준다.
  • (10) : 이제 페이징을 하기 위한 변수는 모두 생성했으니 객체로 리턴한다.

✔ 템플릿 만들기

if startPage > maxPost // (1)
	a.prev_btn.pageBtn(href=`${routes.home}?page=${startPage-1}`)
		i.fas.fa-angle-double-left
else // (2)
	a.prev_btn.pageBtn.disabled(href="javascript:void(0);")
		i.fas.fa-angle-double-left
-for (let i = startPage; i <= endPage ; i++) // (3)
	if i === currentPage // (4)
		a.pageBtn.current(href=`${routes.home}?page=${i}`)
			span=i
	else
		a.pageBtn(href=`${routes.home}?page=${i}`)
			span=i       
if endPage < totalPage // (5)
	a.next_btn.pageBtn(href=`${routes.home}?page=${endPage+1}`)
		i.fas.fa-angle-double-right
else // (6)
	a.next_btn.pageBtn.disabled(href="javascript:void(0);")
		i.fas.fa-angle-double-right

✔ 화면 구성

  • 다음 페이지 버튼으로 넘어갈 수 있다. (만약 현재 페이지가 15일 경우 21페이지로) 조건을 만족하지 못 할 경우 disiable 처리한다. (만약 위치한 페이지가 페이징할 마지막 페이지들에 위치할 경우)
  • 이전 페이지 버튼으로 넘어갈 수 있다. (만약 현재 페이지가 15일 경우 10페이지로) 조건을 만족하지 못 할 경우 disiable 처리한다. (만약 위치한 페이지가 페이징할 첫번째 페이지들에 위치할 경우)
  • 현재 페이지는 .current 선택자를 이용하여 다른 색으로 표시한다.

✔ 설명

  • (1) : startPage가 maxPost 보다 크다는 것은 현재 페이지가 페이징할 첫번째 페이지가 아님을 의미한다.(11이상) 이전페이지 버튼을 활성화 한다.
  • (2) : 조건을 만족하지 못 할 경우 비활성화 한다.
  • (3) : startPage부터 endPage까지 반복하며 페이지 버튼을 출력한다.
  • (4) : 현제 페이지의 경우 다른 색상으로 표시한다.
  • (5) : endPage가 totalPage보다 작다는 것은 현재 페이지가 페이징할 마지막 페이지가 아님을 의미한다. 다음페이지 버튼을 활성화 한다.
  • (6) : 조건을 만족하지 못 할 경우 비활성화 한다.

끄읏!

profile
ansuzh
post-custom-banner

2개의 댓글

comment-user-thumbnail
2021년 4월 29일

잘봤습니다 감사해요 :) 제 프로젝트에 도움이 됐습니다

1개의 답글