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

소스코드

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

이 게시물의 소스코드는 게시판 만들기(고급) / 게시판 - 페이지 기능 만들기 1에서 이어집니다.

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

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

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

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

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


이전 강의에서 게시판에 페이지 기능을 추가해 봤습니다. 페이지 기능은 /posts route에 page, limit query string을 추가하여 조정하는데, /posts 페이지를 벗어나는 순간 이 정보들을 잃게 됩니다. 예를 들어 한페이지에 5개의 게시물만 표시하도록 했더라도, 게시물을 클릭하여 post의 show 페이지로 갔다가 back 버튼을 눌러서 다시 post의 index 페이지로 돌아오면 limit=5 query string이 사라져서 다시 10개의 게시물이 표시되게 됩니다.

이걸 고치려면 여러가지 방법이 있는데, 저는 post와 관련된 모든 route, view에 이전 query string들을 계속하여 전달하는 방법을 사용하겠습니다.

예를 들어 index에서 show로 넘어갈 때 limit이나 page의 querystring을 전달해주고, show에서 index로 넘어 올 때 다시 limit과 page를 전달해서 페이지에 표시될 게시물 수, 현재페이지 정보를 유지할 수 있게 하는 방법입니다.

폴더 구조

코드 - js

// util.js

...

util.getPostQueryString = function(req, res, next){
  res.locals.getPostQueryString = function(isAppended=false, overwrites={}){    
    var queryString = '';
    var queryArray = [];
    var page = overwrites.page?overwrites.page:(req.query.page?req.query.page:'');
    var limit = overwrites.limit?overwrites.limit:(req.query.limit?req.query.limit:'');

    if(page) queryArray.push('page='+page);
    if(limit) queryArray.push('limit='+limit);

    if(queryArray.length>0) queryString = (isAppended?'&':'?') + queryArray.join('&');

    return queryString;
  }
  next();
}

module.exports = util;

util.getPostQueryString함수는 res.localsgetPostQueryString함수를 추가하는 middleware입니다. 이렇게 res.locals에 추가된 변수나 함수는 view에서 바로 사용할 수 있고, res.locals.getPostQueryString의 형식으로 route에서도 사용할 수 있게 됩니다.

res.locals.getPostQueryString함수의 기본역할은 req.query로 전달 받은 query에서 page, limit을 추출하여 다시 한줄의 문자열로 만들어 반환하는 것입니다.

2개의 파라메터를 optional로 받는데, 첫번째로 파라메터는 생성할 query string이 기존의 query string에 추가되는(appended) query인지 아닌지를 boolean으로 받습니다. 만약 추가되는 query라면 '&'로 시작하고, 아니라면 '?'로 시작하는 query string을 만듭니다.

두번째 파라메터는 req.query의 page나 limit을 overwrite하는 파라메터입니다. 예를들어 req.query.page의 값를 무시하고 page를 무조건 1로 하는 query를 만들고 싶다면 {page:1}을 전달하면 됩니다.

** util.getPostQueryString함수에 대한 더 자세한 설명이 필요하시면 이 댓글을 참고해보세요.

// index.js

...
var passport = require('./config/passport');
var util = require('./util'); <!-- 1 -->
...

// Routes
app.use('/', require('./routes/home'));
app.use('/posts', util.getPostQueryString, require('./routes/posts')); <!-- 1 -->
app.use('/users', require('./routes/users'));

...

util.getPostQueryString미들웨어를 posts route이 request되기 전에 배치하여 모든 post routes에서 res.locals.getPostQueryString를 사용할 수 있게 하였습니다.

// routes/posts.js

...

// create
      ...
      return res.redirect('/posts/new'+res.locals.getPostQueryString()); // 1
    }
    res.redirect('/posts'+res.locals.getPostQueryString(false, {page:1})); //2

// update
      ..
      return res.redirect('/posts/'+req.params.id+'/edit'+res.locals.getPostQueryString()); // 1
    }
    res.redirect('/posts/'+req.params.id+res.locals.getPostQueryString()); // 1
  ...

// destroy
    ...
    res.redirect('/posts'+res.locals.getPostQueryString()); // 1
  ...

1. post의 routes에서 redirect가 있는 경우 res.locals.getPostQueryString함수를 사용하여 query string을 계속 유지하도록 합니다. 물론 해당 route로 page, limit query string들이 전달되어야 합니다. 이 부분은 view에서 설정해줘야 합니다.

여기서 생성되는 query string은 기존의 query string에 추가되는 게 아니고(isAppended = false), 값을 overwrite하지도 않으므로 파라메터 전달 없이 res.locals.getPostQueryString()로 호출합니다. 

2. 새 글을 작성한 후에는 무조건 첫번째 page를 보여주도록 page query를 1로 overwrite해줍니다. overwrite은 res.locals.getPostQueryString함수의 두번째 파라메터이죠. 첫번째 파라메터는 optional이지만 첫번째 파라메터없이 두번째 파라메터를 전달할 수 없으므로, (false, {page:1})를 사용합니다.

코드 - ejs

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

              ...
     <!-- 1 --> <a href="/posts/<%= post._id %><%= getPostQueryString() %>"><div class="ellipsis"><%= post.title %></div></a>
              ...

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

            <li class="page-item <%= previousBtnEnabled?'':'disabled' %>">
   <!-- 3 --> <a class="page-link" href="/posts<%= getPostQueryString(false, {page:currentPage-1}) %>" <%= previousBtnEnabled?'':'tabindex=-1' %>>«</a>
            </li>
            <% for(i=1;i<=maxPage;i++){ %>
              <% if(i==1 || i==maxPage || (i>=currentPage-offset && i<=currentPage+offset)){ %>
     <!-- 4 --> <li class="page-item <%= currentPage==i?'active':'' %>"><a class="page-link" href="/posts<%= getPostQueryString(false, {page:i}) %>"> <%= i %> </a></li>
              ...
            <% } %>
            <li class="page-item <%= nextBtnEnabled?'':'disabled' %>">
   <!-- 5 --> <a class="page-link" href="/posts<%= getPostQueryString(false, {page:currentPage+1}) %>" <%= nextBtnEnabled?'':'tabindex=-1' %>>»</a>
            </li>
          ...

1. 게시판에서 게시물 제목을 눌러 show route으로 가는 경우 page, limit query string을 전달해 줍니다. view에서는 res.locals의 항목들을 바로 사용할 수 있기때문에 res.locals.getPostQueryString가 아니라 getPostQueryString로 호출합니다.

2. New 버튼을 눌러 new route으로 가는 경우에도 마찬가지입니다.

3. pagination의 이전 페이지 버튼은 page가 현재페이지-1로 되도록overwrite합니다. 

4. pagination의 페이지 버튼들입니다 page가 i로 되도록 overwrite합니다.

5. pagination의 다음 페이지 버튼입니다. page가 현재페이지+1로 되도록 overwrite합니다.

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

    ...

      <form action="/posts<%= getPostQueryString() %>" method="post"> <!-- 1 -->
        ...

 <!-- 2 --> <a class="btn btn-primary" href="/posts<%= getPostQueryString() %>">Back</a>          ...

1,2. new view에서는 form의 action url과 뒤로가기 버튼에 getPostQueryString함수가 사용되었습니다.

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

      ...
        <a class="btn btn-primary" href="/posts<%= getPostQueryString() %>">Back</a> <!-- 1 -->
        <% if(isAuthenticated && post.author && currentUser.id == post.author.id){ %>
<!-- 2 --><a class="btn btn-primary" href="/posts/<%= post._id %>/edit<%= getPostQueryString() %>">Edit</a>
<!-- 3 --><form action="/posts/<%= post._id %>?_method=delete<%= getPostQueryString(true) %>" method="post" class="d-inline">
            <a class="btn btn-primary" href="javascript:void(0)" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a>
          </form>
        <% } %>
      ...

1,2,3. show view에서는 뒤로가기버튼, 수정 버튼, 삭제버튼의 form에getPostQueryString함수가 사용되었습니다. 삭제버튼의 form에는 이미 ?_method=delete query가 달려 있으므로 isAppended파라메터가 true가 되어야 합니다.

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

    ...

      <form action="/posts/<%= post._id %>?_method=put<%= getPostQueryString(true) %>" method="post"> <!-- 1 --> 
        ...

<!-- 2 --><a class="btn btn-primary" href="/posts<%= getPostQueryString() %>">Back</a>
          ...

1,2. edit view에서는 form의 action url과 뒤로가기 버튼에 getPostQueryString함수가 사용되었습니다.

실행 결과

게시판에 표시할 페이지 수, 페이지를 변경한 후에 게시물을 클릭해 봅시다.

url에 page, limit의 query 로 추가되어 있는 것을 볼 수 있습니다.

edit버튼을 눌러봅시다.

여전히 page, limit의 query가 유지되고 있습니다.

back버튼을 눌러 게시물 목록까지 돌아옵시다.

page, limit이 query에 의해 동일한 목록이 표시됩니다.

마치며...

이제 제법 쓸만한 게시판이 되어 가는 것 같습니다. 다음에는 게시물 검색 기능을 추가해 보겠습니다. 수고하셨습니다!

댓글

J
Jake Lyu 2020.04.25
getPostQueryString 함수를 이해하기 너무 어렵습니다 ㅠㅠ 갑자기 바보된 느낌..
1. function(   )   req, res 넣다가 isAppended = false, overwrites={ }  이 인자들은 어디서 온 것인가요..?
2. var page = overwrites.page ? overwrites.page : req.query.page ? req.query.page : "";  전혀 이해가 안됩니다 ㅠㅠ overwrites.page? : ?  아주 기초 수준에서 설명이 가능한지 부탁드립니다
I
Ian H 2020.04.27
@Jake Lyu,
안녕하세요  util.getPostQueryString함수는 res.locals.getPostQueryString함수를 생성해주는 Express의 미들웨어(middle ware) 함수입니다.
미들웨어는 'app.use(미들웨어함수)'의 형태로 사용되고 서버에 request가 오는 경우 항상 실행됩니다.(물론 이전의 미들웨어에서 res.end()로 request가 종료되지 않는 한에서) 위 예제의 index.js를 살펴보면 'app.use()'가 사용된 부분들이 보이는데 다 미들웨어들입니다.
미들웨어는 세가지인자를 가지는데 순서대로 request 오브젝트, response 오브젝트 , next 함수입니다. request 오브젝트는 request와 관련된 정보들을, response 오브젝트는 response와 관련된 정보들을, next 함수는 현재 미들웨어에서 다음미들웨어로 넘어가는 함수입니다.(미들웨어들은 모두 비동기 함수(async)로 next를 호출해 주지 않으면 다음 미들웨어로 넘어가지 않고 영원히 그자리에서 머무릅니다!) 이 세가지 인자들은 util.getPostQueryString에서 (req, res, next)로 사용되었습니다.
res.locals는 view에서도 바로 호출할 수 있는 특수한 기능을 가진 object입니다. 즉 res.locals.getPostQueryString함수는 view에서 <%= getPostQueryString() %>와 같이 바로 호출할 수 있습니다.
크게 봤을 때 util.getPostQueryString 함수는 다음과 같은 구조입니다. util.getPostQueryString = function(req, res, next){   res.locals.getPostQueryString = function ...   next(); }
즉 res.locals.getPostQueryString에 함수를 만들어 주고 next로 종료하는 구조이죠. res.locals는 request마다 초기화 되기때문에 모든 request에서 사용하려면 이렇게 request가 올 때마다 생성해 주어야 합니다.
다음으로 res.locals.getPostQueryString를 살펴봅시다. 함수의 인자에 =를 넣는 것을 처음보신것 같은데요, 함수 인자에 =를 사용하게 초기값을 설정해 줄 수 있습니다. 위처럼 res.locals.getPostQueryString = function(isAppended=false, overwrites={}){ ... }로 인자를 설정하는 경우
res.locals.getPostQueryString()로 함수를 호출하면 아무런 인자를 전달해 주지 않았지만 isAppended는 false로, overwrites를 {}로 하여 함수를 시작하게 됩니다. res.locals.getPostQueryString(true)의 경우 isAppended는 인자를 받아서 true가 되고, overwrites는 아무런 값을 받지 않았으므로 {}로 초기화됩니다. var page = overwrites.page?overwrites.page:(req.query.page?req.query.page:''); 는 삼항방정식입니다.
삼항 방정식은 '조건?조건이참인경우:조건이거짓인경우'의 형태입니다. 예를 들어 var a = 1<2 ? 3 : 4;라고 한다면 조건이 1<2이고, 조건이 참이므로 a에는 3대입됩니다.
위 코드에서는 조건이 거짓인 경우에 다시 삼항방정식이 들어가서 다시 새로운 조건을 보는 식입니다. 즉 overwrites.page가 있다면 해당 값이 page에 들어가고, 없다면 req.query.page의 값이 있는지 확인 후 있다면 req.query.page의 값이 들어가고, 없다면 ""가 들어갑니다.
이해가 잘 안되신다면 다시 질문주세요^^
J
Jake Lyu 2020.04.27
@Ian H,
감사합니다. 너무 이해하기 쉽게 자세히 설명해주셔서 감동의 눈물을 흘렸습니다. ㅋㅋ
I
Ian H 2020.04.27
@Jake Lyu,
ㅋㅋ 이해하셨다니 다행입니다!
특대갈비 2020.06.29
왜 저 getPostQueryString 를 못읽을까요....?
I
Ian H 2020.06.29
@특대갈비,
제 강의의 index.js파일처럼
app.use('/comments', util.getPostQueryString, require('./routes/comments'));
이렇게 util.getPostQueryString을 해당 route의 middle-ware로 사용해 주셔야 해당코드를 사용할 수 있습니다^^
댓글쓰기

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

UP