게시판 - 검색 기능 만들기 1 (제목, 본문 검색)

소스코드

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

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

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

git reset --hard
git pull
git reset --hard cc05107
git reset --soft fc06140
npm install
atom .

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

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

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


게시물 검색 기능을 만들어 봅시다.

검색타입(searchType), 검색어(searchText)를 query string으로 /posts route에 전달받아 해당 게시물만 보여주는 기능을 추가하고, post의 index view에 해당 기능을 수행할 수 있도록 form을 만듭니다. 검색타입은 'title'(제목), 'body'(본문), 'title,body'로 'title,body'는 title이나 body 둘 중 하나에만 검색어가 포함되어도 검색됩니다.

검색어는 해당항목의 일부분과 일치하면 해당 게시물이 검색됩니다. 예를 들어 title이 'test'인 게시물을 검색하는 경우, 제목이 정확히 'test'인 게시물만 찾는 것이 아니라, title에 'test'가 들어간 모든 게시물을 찾게 됩니다. 본문도 마찬가지입니다.

폴더 구조

코드 - 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:'');
    var searchType = overwrites.searchType?overwrites.searchType:(req.query.searchType?req.query.searchType:''); // 1
    var searchText = overwrites.searchText?overwrites.searchText:(req.query.searchText?req.query.searchText:''); // 1

    if(page) queryArray.push('page='+page);
    if(limit) queryArray.push('limit='+limit);
    if(searchType) queryArray.push('searchType='+searchType); // 1
    if(searchText) queryArray.push('searchText='+searchText); // 1

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

    return queryString;
  }
  next();
}

module.exports = util;

1. 페이지 기능과 마찬가지로 검색에 관련된 query string들이 게시물과 관련된 route들에서 계속 따라다닐 수 있도록 getPostQueryString 함수에 searchTypesearchText를 추가해줍니다.

// routes/post.js

...

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

  var searchQuery = createSearchQuery(req.query); // 1

  var skip = (page-1)*limit;
  var count = await Post.countDocuments(searchQuery); // 1-1
  var maxPage = Math.ceil(count/limit);
  var posts = await Post.find(searchQuery) // 1-2
    .populate('author')
    .sort('-createdAt')
    .skip(skip)
    .limit(limit)
    .exec();

  res.render('posts/index', {
    posts:posts,
    currentPage:page,
    maxPage:maxPage,
    limit:limit,
    searchType:req.query.searchType, // 2
    searchText:req.query.searchText  // 2
  });
});

...

// create
router.post('/', util.isLoggedin, function(req, res){
  req.body.author = req.user._id;
  Post.create(req.body, function(err, post){
    ...
    res.redirect('/posts'+res.locals.getPostQueryString(false, { page:1, searchText:'' })); // 3
  });
});

...

function createSearchQuery(queries){ // 4
  var searchQuery = {};
  if(queries.searchType && queries.searchText && queries.searchText.length >= 3){
    var searchTypes = queries.searchType.toLowerCase().split(',');
    var postQueries = [];
    if(searchTypes.indexOf('title')>=0){
      postQueries.push({ title: { $regex: new RegExp(queries.searchText, 'i') } });
    }
    if(searchTypes.indexOf('body')>=0){
      postQueries.push({ body: { $regex: new RegExp(queries.searchText, 'i') } });
    }
    if(postQueries.length > 0) searchQuery = {$or:postQueries};
  }
  return searchQuery;
}

1. 실제 게시물 검색은 Post.find(검색_쿼리_오브젝트)에 어떤 검색_쿼리_오브젝트가 들어가는지에 따라 결정됩니다. {title:"test title"}이라는 object가 들어가면 title이 정확히 "test title"인 게시물이 검색되고, {body:"test body"}라는 object가 들어가면 body가 정확히 "test body"인 게시물이 검색됩니다. 이처럼 검색기능에서는 검색_쿼리_오브젝트를 만드는 것이 중요한데, 이것을 만들기 위해 createSearchQuery함수를 만들고 이 함수를 통해 생성된 검색_쿼리_오브젝트searchQuery 변수에 담았습니다.

해당 함수의 코드는 4번에서 살펴보고, 여기서는 해당 함수를 통해 생성된 검색_쿼리_오브젝트searchQuery 변수에 넣었습니다.

1-1. 해당 검색_쿼리_오브젝트를 사용해서 전체 게시물 수와 게시물을 구합니다.

2. view에서 검색 form에 현재 검색에 사용한 검색타입과 검색어를 보여줄 수 있게 해당 데이터를 view에 보냅니다.

3. 새 글을 작성하면 검색 결과를 query string에서 제거하여 전체 게시물이 보이도록 합니다.

4. createSearchQuery함수의 코드를 살펴봅시다.

function createSearchQuery(queries){
  var searchQuery = {};
  if(queries.searchType && queries.searchText && queries.searchText.length >= 3){ // 1
    var searchTypes = queries.searchType.toLowerCase().split(',');
    var postQueries = [];
    if(searchTypes.indexOf('title')>=0){
      postQueries.push({ title: { $regex: new RegExp(queries.searchText, 'i') } }); // 2
    }
    if(searchTypes.indexOf('body')>=0){
      postQueries.push({ body: { $regex: new RegExp(queries.searchText, 'i') } });
    }
    if(postQueries.length > 0) searchQuery = {$or:postQueries}; // 3
  }
  return searchQuery;
}

1. query에 searchType, searchText가 존재하고, searchText가 3글자 이상인 경우에만 search query를 만들고, 이외의 경우에는 {}를 전달하여 모든 게시물이 검색되도록 합니다.

2. {$regex: Regex_오브젝트 }를 사용해서 regex 검색을 할 수 있습니다. 'i'는 대소문자를 구별하지 않는다는 regex의 옵션입니다. $regex query의 정확한 사용법은 https://docs.mongodb.com/manual/reference/operator/query/regex 에서 볼 수 있습니다.

3. {$or: 검색_쿼리_오브젝트_배열 }을 사용해서 or 검색을 할 수 있습니다. $or query의 정확한 사용법은 https://docs.mongodb.com/manual/reference/operator/query/or 에서 볼 수 있고 $and, $nor, $not query도 함께 공부해둡시다.

코드 - ejs

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

      ...

      <form action="/posts" method="get" class="post-index-tool"> <!-- 1 -->
        <div class="form-row">

          <div class="form-group col-3">
            ...
          </div>

          <div class="form-group col-9"> <!-- 2 -->
            <label>Search</label>
            <div class="input-group">
              <select name="searchType" class="custom-select">
                <option value="title,body" <%= searchType=='title,body'?'selected':'' %>>Title, Body</option>
                <option value="title" <%= searchType=='title'?'selected':'' %>>Title</option>
                <option value="body" <%= searchType=='body'?'selected':'' %>>Body</option>
              </select>
              <input minLength="3" type="text" name="searchText" value="<%= searchText %>">
              <div class="input-group-append">
                <button class="btn btn-outline-primary" type="submit">search</button>
              </div>
            </div>
          </div>

        </div>
      </form>

    ...

1. 검색창의 사이즈를 조절하기 위해 post-index-tool CSS class를 추가했습니다.

2. 검색 form입니다. Bootstrap의 input group(https://getbootstrap.com/docs/4.1/components/input-group)을 사용하여 검색어 입력 form과 search 버튼을 이어붙였습니다.

코드 - css

/* public/css/master.css */

.post-index-tool {
  max-width: 450px;
}
.post-index-tool input[type='text']{
  max-width: 130px;
}

실행결과

게시판 아래에 검색 form이 생겼습니다. search type을 'Title, Body'로 하고, '6 title'를 검색해 봅시다.

'6 title'이 제목에 포함된 'Test Post 16 Title' 게시물과 'Test Post 6 Title' 게시물이 검색되었습니다.

마치며

$regex, $or같은 mongo DB 쿼리에 대해 처음으로 다루어 봤는데, https://docs.mongodb.com/manual/reference/operator/query 를 통해 다른 쿼리들도 한번 쯤 눈에 익혀둡시다

댓글

댓글쓰기

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

UP