게시판 - 페이지 기능 만들기 1

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기 / 게시판 - 접근제한에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 7f522a4
git reset --soft c546f73
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 7f522a4
git reset --soft c546f73
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/7f522a4f9dd31be921a944c877f31734f3041f8f


지금은 페이지 기능이 없어서 아무리 게시물이 많아도 post의 index 한 페이지에 모든 게시물이 나타납니다. 일반적으로 게시판이라면 페이지 기능이 있어서 한 페이지에 일정한 수의 게시물만 보여주죠.

1. post의 index route(/posts)에 page, limit을 query로 추가하여, /posts?page=2&limit=10의 형태로 한페이지에 표시될 게시물 수(limit)와 보여줄 페이지(page)를 정하여 해당 게시물들을 보여주는 기능을 추가해 봅시다.

2. 다음으로 post의 index view에 다음페이지, 이전페이지, 페이지 번호등을 나열하여 이용자가 페이지를 이동할 수 있도록 합니다. Bootstrap의 pagination 컴포넌트(https://getbootstrap.com/docs/4.1/components/pagination)를 사용합니다.

3. 마지막으로 post의 index view에 한페이지에 몇개의 게시물을 보여줄 지를 정하는 drop-down메뉴를 추가해 봅시다.

위 기능들을 추가하여 아래와 같이 게시판을 업그레이드해봅시다.

폴더 구조

코드 - js

post index route를 먼저 살펴봅시다.

// routes/posts.js

// Index
router.get('/', async function(req, res){ // 1
  var page = Math.max(1, parseInt(req.query.page));   // 2
  var limit = Math.max(1, parseInt(req.query.limit)); // 2
  page = !isNaN(page)?page:1;                         // 3
  limit = !isNaN(limit)?limit:10;                     // 3

  var skip = (page-1)*limit; // 4
  var count = await Post.countDocuments({}); // 5
  var maxPage = Math.ceil(count/limit); // 6
  var posts = await Post.find({}) // 7
    .populate('author')
    .sort('-createdAt')
    .skip(skip)   // 8
    .limit(limit) // 8
    .exec();

  res.render('posts/index', {
    posts:posts,
    currentPage:page, // 9
    maxPage:maxPage,  // 9
    limit:limit       // 9
  });
});

...

1. async 키워드가 function 키워드 앞에 추가되었습니다. 이 함수안에는 await 키워드를 사용하는데, await 키워드를 사용하는 함수는 반드시 async 키워드를 function 키워드 앞에 붙여야 합니다. await 키워드가 하는 일은 5번에서 설명합니다.

2. Query string으로 전달받은 page, limit을 req.query를 통해 읽어옵니다.
parseInt함수를 사용한 이유: Query string은 문자열로 전달되기 때문에 숫자가 아닐 수도 있고, 정수(소수점이 없음)를 읽어내기 위해 사용했습니다.
Math.max함수를 사용한 이유: page, limit은 양수여야 합니다 최소 1이 되어야 합니다.

3. 만약 정수로 변환될 수 없는 값이 page, limit에 오는 경우 기본값을 설정해 줍니다. 이 값은 해당 query string이 없는 경우에도 사용됩니다.

4. skip은 무시할 게시물의 수를 담는 변수입니다. 페이지당 10개의 게시물이 있고, 현재 3번째 페이지를 만들려면, DB에서 처음 20개의 게시물을 무시하고 21번째부터 10개의 게시물을 보여주는 것이죠.

5. Promise 앞에 await키워드를 사용하면, 해당 Promise가 완료될 때까지 다음 코드로 진행하지 않고 기다렸다가 해당 Promise가 완료되면 resolve된 값을 반환(return)합니다. Post.countDocuments({}) 함수를 사용해서 {}에 해당하는({} == 조건이 없음, 즉 모든) post의 수를 DB에서 읽어 온 후 count변수에 담았습니다.

6. 전체 게시물 수(count)를 알고, 한페이지당 표시되야 할 게시물의 수(limit)을 알면, 전체 페이지 수를 계산할 수 있습니다. 이 값을 maxPage변수에 담습니다.

7. 기존의 Post.find({})await를 사용하여 검색된 posts를 바로 변수에 담을 수 있게 하였습니다.

8. skip함수는 일정한 수만큼 검색된 결과를 무시하는 함수, limit함수는 일정한 수만큼만 검색된 결과를 보여주는 함수입니다.

9. 현재 페이지 번호(currentPage), 마지막 페이지번호(maxPage), 페이지당 보여줄 게시물 수(limit)은 view로 전달하여 view에서 사용할 수 있게 합니다.

코드 - ejs

<!-- views/posts/index.ejs -->

      ...

      </table>

      <div class="row mb-3">

        <div class="col-2"> <!-- 1 -->
          <% if(isAuthenticated){ %>
            <a class="btn btn-primary" href="/posts/new">New</a>
          <% } %>
        </div>

        <nav class="col-8">
          <% <!-- 2 -->
            var offset = 2;
            var previousBtnEnabled = currentPage>1;
            var nextBtnEnabled = currentPage<maxPage;
          %>
<!-- 3 --><ul class="pagination pagination-sm justify-content-center align-items-center h-100 mb-0">            <li class="page-item <%= previousBtnEnabled?'':'disabled' %>">
              <a class="page-link" href="/posts?page=<%= currentPage-1 %>&limit=<%= limit %>" <%= previousBtnEnabled?'':'tabindex=-1' %>>«</a>
            </li>
            <% for(i=1;i<=maxPage;i++){ %>
              <% if(i==1 || i==maxPage || (i>=currentPage-offset && i<=currentPage+offset)){ %>
                <li class="page-item <%= currentPage==i?'active':'' %>"><a class="page-link" href="/posts?page=<%= i %>&limit=<%= limit %>"> <%= i %> </a></li>
              <% } else if(i==2 || i==maxPage-1){ %>
                <li><a class="page-link">...</a></li>
              <% } %>
            <% } %>
            <li class="page-item <%= nextBtnEnabled?'':'disabled' %>">
              <a class="page-link" href="/posts?page=<%= currentPage+1 %>&limit=<%= limit %>" <%= nextBtnEnabled?'':'tabindex=-1' %>>»</a>
            </li>
          </ul>
        </nav>

      </div>

      <form action="/posts" method="get"> <!-- 4 -->
        <div class="form-row">

          <div class="form-group col-3">
            <label>Show</label>
            <select name="limit" class="custom-select" onchange="this.parentElement.parentElement.parentElement.submit()">
              <option value="5" <%= limit==5?'selected':'' %>>5 posts</option>
              <option value="10" <%= limit==10?'selected':'' %>>10 posts</option>
              <option value="20" <%= limit==20?'selected':'' %>>20 posts</option>
            </select>
          </div>

        </div>
      </form>

    </div>
  </body>
</html>

1. New 버튼을 pagination과 같은 줄에 놓기 위해 Bootstrap grid(https://getbootstrap.com/docs/4.1/layout/grid) css (row, col-?)를 사용하였습니다.

2. pagination에 사용될 변수들입니다.
offset: 현재페이지 좌우로 몇개의 페이지들을 보여줄지를 정하는 변수입니다.
previousBtnEnabled: 이전 페이지 버튼은 현재페이지가 1보다 큰 경우에만 활성화됩니다.
nextBtnEnabled: 다음 페이지 버튼은 현재페이지가 마지막페이지(maxPage)보다 작은 경우에만 활성화됩니다.

3. 기본적인 구조는 https://getbootstrap.com/docs/4.0/components/pagination에서 가져왔고, 페이지번호 버튼 위치에 맞게 이동해야할 route를 /posts?page=page&limit=limit 만들어 줍니다.

중간의 페이지번호 버튼들을 출력하는 반복문의 조건을 잘 살펴봅시다.

if(i==1 || i==maxPage || (i>=currentPage-offset && i<=currentPage+offset))

만약, 페이지번호 버튼이 1페이지, 혹은 마지막 페이지 인 경우, 혹은 현재페이지와 offset 이내의 차이가 있는 경우에는 페이지 번호 버튼을 만듭니다.

else if(i==2 || i==maxPage-1)

만약, 1번 조건에 페이지번호 2와 마지막 바로 이전 페이지번호 버튼이 맞지 않는다면 ... 를 보여줍니다.

위 두 조건에 모두 맞지 않는 페이지번호 버튼들은 화면에 표시하지 않습니다.

4. 화면에 표시될 게시물의 수를 바꿀 수 있는 form을 만들었습니다. form의 method를 get으로 하면, form 항목들이 action route의 query string으로 전달됩니다. onchange항목을 사용하여 drop-down의 값이 변경되면 자동으로 form이 submit되도록 하였습니다.

form-rowcol-3를 사용하였는데, 나머지 공간에는 나중에 검색 기능을 넣기 위해 비워뒀습니다.

실행결과

게시판에 접속하면 처음에는 /posts route에 page, limit query가 없으므로 post index route에서 page = 1, limit = 10을 기본값으로 하여 페이지를 생성합니다.

drop-down 메뉴를 이용해서 페이지당 표시될 게시물을 5개로 바꿔 봅시다.

주소가 /posts?limit=5로 바뀌고 5개의 게시물이 표시됩니다. pagination의 페이지 번호 버튼이 어떻게 바뀌는지도 잘 보세요.

다음페이지로 이동해 봅시다.

주소가 /posts?page=2&limit=5로 바뀌었습니다.

주소에 넣는 값에 따라 게시판에 보이는 결과를 마음대로 바꿀 수 있습니다. limit을 1로 페이지를 5로 해봅시다.

페이지 버튼이 너무 많아지만 첫번째 페이지, 마지막 페이지, 현재페이지와 3이상 차이나는 페이지들은 ...로만 표시됩니다. drop-down메뉴에는 limit = 1 옵션이 없으므로 그냥 첫번째 옵션이 표시됩니다.

마치며..

pagination을 통해 페이지를 자유롭게 이동할 수 있게 되었습니다. 하지만 지금의 게시판은 2번째 페이지의 게시물에 들어갔다가 back버튼을 누르면 1번째 페이지로 나오게 됩니다. 다음 강의에서는 이 부분을 고쳐보도록 하겠습니다.

댓글

K
Kairo 2020.02.02
질문이 있습니다!  저번에 질문에 답 해 주실때 고급 마지막 파일 깃허브(다댓글페이지)에 초급,고급게시판의 기능들이 다 있다고 들었습니다. 제가 고급게시판의 마지막 다댓글 파일을 다운받아서  처음 초급 게시판 부터 보고있었습니다. 초급 게시판의 게시판 접근제한의 코드가 작성되어 있지 않아 ian h님의 블로그의 글을 따라 작성하였습니다. paginamtion의 기능도 코드에 적혀있지않아 ian h 님이 하신 방법 그대로 복사 붙여넣기를 하였습니다. post 페이지에서 확인을 하려고 했는데 오류가 발생하여 질문 드립니다!
routes->posts.js에서   res.render('posts/index', {     posts:posts,     currentPage:page, // 9     maxPage:maxPage,  // 9     limit:limit       // 9       }); 잘 따라서 작성하였고.
views->posts->index.ejs에서 <% %>이부분에서도 그대로 복사하여서 선언이 된 것 같은데 어디가 문제인지 모르겠습니다.! 
에러내용입니다. 49|     </div>     50|     <nav class="col-8">  >> 51|       <%     52|       var offset = 2;     53|       var previousBtnEnabled = currentPage>1;     54|       var nextBtnEnabled = currentPage<maxPage;
currentPage is not defined
감사합니다.
I
Ian H 2020.02.03
@Kairo,
안녕하세요^^ 제 강의의 코드는 이전 강의의 코드에서 이어지기 때문에 대댓글 강의의 소스코드(https://github.com/a-mean-blogger/board/tree/104fe361b1cf24ef7d9ef922c6d87d86a6a94f88)에 접근제한 및 페이지 기능을 포함한 모든 이전 강의의 코드들이 들어 있습니다. 
코드의 오류는 단편적인 코드만으로는 원인을 파악하기 힘듭니다. 우선 https://www.a-mean-blog.com/ko/blog/단편강좌/_/node-js-디버깅-방법 을 통해 post route에서 'currentPage:page, // 9'의 page 변수의 값이 정상적으로 생성되는지를 확인하여 원인분석을 해보세요. 디버깅을 하는 법을 익히는 것도 중요하니까 직접 해보는게 좋습니다.
아무리 해도 원인파악이 안되고 도움이 필요한 경우에는 Kairo님의 코드를 github에 올리신 후 주소를 알려주시면 제가 한번 체크해 보겠습니다.
화이팅!
K
Kairo 2020.02.03
@Ian H,
해결하였습니다!! 감사합니다!   그리고 제가 여러번 ian h님의 파일을 받다보니 대댓글 소스코드를 받지 않은 것 같습니다 
I
Ian H 2020.02.04
@Kairo,
해결하셨다니 다행입니다!
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP