게시판 - 검색 기능 만들기 2 (작성자 검색, 검색어 하일라이트)

소스코드

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

이 게시물의 소스코드는 게시판 만들기(고급) / 게시판 - 검색 기능 만들기 1 (제목, 본문 검색)에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 468999e
git reset --soft cc05107
npm install
atom .

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

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

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


지난번 강의에 이어서 작성자 검색 기능을 추가하고, 게시판의 검색 결과에 검색어를 하일라이트해주는 기능을 추가해 봅시다.

작성자 검색은 작성자의 username 정확히 일치하는 경우와 작성자의 username이 일부만 일치하는 경우로 나눌텐데, 전자를 'author!', 후자를 'author'로 searchType query string에 전달합니다.

검색어 하일라이트하는 기능은 서버에서 해당 기능을 수행하는 것이 아니라, query string의 searchType, searchText를 사용하여 클라이언트 웹 브라우저에서 자바스크립트로 해당 부분을 찾아 css 스타일을 변경하는 방법으로 만들어 봅시다.

폴더 구조

코드 - js

...
var User = require('../models/User'); // 1
var util = require('../util');

// Index // 3

  ...
  limit = !isNaN(limit)?limit:10;

  var skip = (page-1)*limit;
  var maxPage = 0;
  var searchQuery = await createSearchQuery(req.query);
  var posts = [];

  if(searchQuery) {
    var count = await Post.countDocuments(searchQuery);
    maxPage = Math.ceil(count/limit);
    posts = await Post.find(searchQuery)
      .populate('author')
      .sort('-createdAt')
      .skip(skip)
      .limit(limit)
      .exec();
  }

  res.render('posts/index', {  
    ...


async function createSearchQuery(queries){ // 2

    ...
    if(searchTypes.indexOf('author!')>=0){ // 2-1
      var user = await User.findOne({ username: queries.searchText }).exec();
      if(user) postQueries.push({author:user._id});
    }
    else if(searchTypes.indexOf('author')>=0){ // 2-2
      var users = await User.find({ username: { $regex: new RegExp(queries.searchText, 'i') } }).exec();
      var userIds = [];
      for(var user of users){
        userIds.push(user._id);
      }
      if(userIds.length>0) postQueries.push({author:{$in:userIds}});
    }
    if(postQueries.length>0) searchQuery = {$or:postQueries}; // 2-3
    else searchQuery = null;                                  // 2-3
  }
  return searchQuery;
}

1. 우리는 username으로 DB에서 해당 user를 찾고, 해당 user의 id를 author로 가지는 post를 찾는 방식으로 작성자 검색을 합니다. user를 검색해야 하니, user모델을 required해야 합니다.

** 이렇게 DB에 query를 두번 보내지 않고 한번에 하는 방법도 물론 있습니다. https://docs.mongodb.com/manual/aggregation 를 봐주세요.

2. createSearchQuery함수 안에서 user모델을 검색하기 때문에 async 함수가 되었습니다.

2-1. searchType이 author!인경우, searchText가 username과 일치하는 user 한명을 찾아 검색 쿼리에 추가합니다.

2-2. searchType이 author인 경우에는 regex를 사용해 searchText가 username에 일부분인 user를 모두 찾아 개별적으로 $in operator(https://docs.mongodb.com/manual/reference/operator/query/in)를 사용하여 검색 쿼리를 만듭니다. 즉 author가 userIds 안(in)에 포함된 경우를 찾는 쿼리입니다.

2-3. 작성자 검색의 경우, 해당 user가 검색된 경우에만 postQueries에 조건이 추가됩니다. 만약 검색 조건과 맞는 user가 하나도 존재하지 않는다면, 게시물 검색결과는 없어야 합니다. 이를 판별하기 위해 검색 조건에 맞는 작성자가 존재하지 않는 경우, searchQuery에 null을 넣었습니다.

3. 2-3에서 설명한 것 처럼, 작성자의 검색결과가 없다면 post를 검색할 필요가 없습니다. 이것을 위해 Post.countDocuments, Post.find 및 관련 코드들을 searchQuery가 존재하는 경우에만 실행하도록 코드를 약간 수정하였습니다.

// public/js/script.js

...

$(function(){
  var search = window.location.search; // 1
  var params = {};

  if(search){ // 2
    $.each(search.slice(1).split('&'),function(index,param){
      var index = param.indexOf('=');
      if(index>0){
        var key = param.slice(0,index);
        var value = param.slice(index+1);

        if(!params[key]) params[key] = value;
      }
    });
  }

  if(params.searchText && params.searchText.length>=3){ // 3
    $('[data-search-highlight]').each(function(index,element){
      var $element = $(element);
      var searchHighlight = $element.data('search-highlight');
      var index = params.searchType.indexOf(searchHighlight);

      if(index>=0){
        var decodedSearchText = params.searchText.replace(/\+/g, ' '); //  3-1
        decodedSearchText = decodeURI(decodedSearchText);
        
        var regex = new RegExp(`(${decodedSearchText})`,'ig'); // 3-2
        $element.html($element.html().replace(regex,'<span class="highlighted">$1</span>'));
      }
    });
  }
});

1. window.location.search에 query string의 정보가 들어 있습니다. ?searchType=title&searchText=text의 형태입니다.

2. 1번을 분석해서 query string을 오브젝트로 바꿔줍니다. 사실 Chrome, Safari같은 브라우저를 사용한다면 이 부분을 직접 코딩할 필요 없이 URLSearchParams(https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) 를 사용할수 있습니다만, IE에서 불가능하기 때문에 이와 비슷한 기능을 직접 코딩을 했습니다.

3. data-search-highlight의 값을 searchType과 비교하여, 일치하는 경우 searchText를 regex로 찾아 해당 텍스트에 highlighted css class를 추가하는 코드입니다.

3-1. searchText에 띄어쓰기가 있는 경우 query string으로 '+'문자가 삽입되기 때문에 이것을 다시 ' '로 바꿔줍니다. 이때 regex에는 g옵션이 들어갑니다. g는 일치하는 여러개의 값을 모두 찾는 옵션으로, 만약 g가 없다면 'abc abc'라는 문자열에 'abc'라는 regex를 검사시키면 첫번째 abc만 찾고 검사가 끝납니다. g옵션이 있어야 모든 abc를 찾습니다.

그리고 다른 '&'등과 같은 특수문자들은 querystring으로 들어갈때 '%26'과 같이 URI encode가 되는데, 이를 다시 decode해줘야 합니다.

3-2. RegExp에 옵션으로 'ig'가 들어갔는데, i와 g 옵션 2가지가 들어간 것입니다. i 옵션은 이전에 써봤듯이 대소문자를 구별하지 않는 옵션입니다.

코드 - ejs

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

        ...

        <tbody>
          <% if(posts == null || posts.length == 0){ %>
            <tr>
              <td colspan=2> There is no data to show :( </td>
            </tr>
          <% } %>
          <% posts.forEach(function(post) { %>
            <tr>
              <td>
                <a href="/posts/<%= post._id %><%= getPostQueryString() %>">
       <!-- 1 --> <div data-search-highlight="title" class="ellipsis"><%= post.title %></div> 
                </a>
              </td>
              <td class="author">
                <a href="/posts<%= getPostQueryString(false, { searchType:'author!', searchText:post.author.username }) %>">
       <!-- 2 --> <div data-search-highlight="author" class="ellipsis"><%= post.author ? post.author.username : "" %></div>                </a>
              </td>
              ...

              <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>
     <!-- 3 --> <option value="author" <%= searchType=='author'?'selected':'' %>>Author</option>              </select>
              ...

1. 게시물 제목에 data-search-highlight="title"가 추가되었습니다. searchType에 title이 포함되는 경우, 여기의 text에서 searchText와 일치하는 부분을 highlight합니다.

2. 이제 게시물 목록에서 작성자 username을 클릭하면 해당 작성자가 검색한 모든 글들을 볼 수 있습니다. 작성자를 클릭하는 경우 author!로 검색하게 되어 작성자 이름이 완전히 일치하는 글만 검색됩니다.

또한 여기에는 data-search-highlight="author"가 추가되어, searchType에 author가 포함되는 경우, 여기의 text에서 searchText와 일치하는 부분을 highlight합니다.

3. 검색form에 작성자(author)옵션을 추가했습니다. 검색창을 통해 작성자를 검색하는 경우에는 !가 없이 author로 검색하게 되어 검색어가 작성자 username의 일부인 모든 게시물을 검색하게 됩니다.

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

      ...

      <div class="card">
        <h5 class="card-header p-2" data-search-highlight="title"><%= post.title %></h5> <!-- 1 -->
        <div class="row">

          <div class="col-md-7 col-lg-8 col-xl-9 order-sm-2 order-md-1">
 <!-- 1 --> <div class="post-body p-2" data-search-highlight="body"><%= post.body %></div>
          </div>

          <div class="col-md-5 col-lg-4 col-xl-3 order-sm-1 order-md-2">
            <div class="post-info card m-2 p-2">
              <div class="border-bottom pb-1 mb-1">
      <!-- 1 --> <span>Author</span> : <span data-search-highlight="author"><%= post.author ? post.author.username : "" %></span>              </div>
              <div><span>Created</span> : <span data-date-time="<%= post.createdAt %>"></span></div>
               ...

1. 게시물 제목, 본문, 그리고 작성자 이름에 각각 알맞는 data-search-highlight가 들어갔습니다.

코드 - css

/* public/css/master.css */

.highlighted {
  background-color: yellow;
}

실행 결과

검색타입은 'Title', 검색어는 '6 title'로 하여 검색을 해 봅시다.

'6 title' 부분이 하일라이트 되었습니다. 게시물을 클릭해봅시다.

게시물에서도 알맞게 하일라이트가 되는지 확인해 봅시다.

다음으로 작성자 검색을 해보겠습니다. test라는 작성자를 만들어 새로운 글을 작성하였습니다. 검색타입은 'Author', 검색어는 'test'로 검색을 해 봅니다.

'test'와 'test1'이 쓴 글들이 모두 표시됩니다. 이제 게시물 목록에서 'test' 작성자를 클릭해 봅시다.

test가 쓴 글만 보이게 됩니다.

마치며...

다음은 '댓글달기' 기능입니다. 감사합니다.

댓글

m
mpkw 2020.01.22
node.js공부 시작한지 3주 정도 됬는데 이렇게 코드 하나하나 알려주는 곳 많지 않더라구요. 공부 시작하자마자 이 블로그를 알았더라면 하는 생각도 듭니다. 여튼 글 잘 보고 공부하고 있습니다. 감사합니다.
I
Ian H 2020.01.22
@mpkw,
분명 그 전에 한 공부덕에 제 코드가 더 쉽게 이해되셨을거에요 제가 설명을 쉽게 잘하는 편이 아니라서요^^;; 감사합니다!
롱건 2020.02.27
정말 정말 최고의 강의 입니다. c,c++, c# 만하다가 자바스크립트로 실무에서 사용하려고 여기저기 다니며 들어두 개념이 잡히질 않았는데  프론트, 백엔드 쪽을 이렇게 완벽하게 설명해주셔서 감을 잡은것 같습니다.  정말 감사합니다. 계속 좋은 강의 부탁드립니다.
I
Ian H 2020.02.27
@롱건,
격려의 말씀 감사합니다^^ 다른 node.js입문자들에게도 소문 좀 부탁드릴게요!
J
Jake Lyu 2020.04.26
/* public/css/master.css
.highlighted {   background-color: yellow; }
/* public/css/master.css   --> */   저 부분 빠져서 컬러가 안입혀지네요 ㅋㅋ
I
Ian H 2020.04.27
@Jake Lyu,
본문 내용은 수정하였습니다! 제보주셔서 감사합니다^^
댓글쓰기

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

UP