본문으로 바로가기

'Node.js 교과서 - 조현영 저'를 활용해 공부했습니다.

 

[Node.js] Express+MongoDB, API 서버 구현하기(1)와 이어집니다.


1. API 구현하기

1) Users

유형 라우트 설명
GET /users 전체 사용자 정보 조회
POST /users 사용자 추가

/routes/users.js

var express = require('express');
var User = require('../schemas/user');

var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  User.find({})
  .then((users) => {
    res.json(users);
  })
  .catch((err)=>{
    console.error(err);
    next(err);
  });
});

router.post('/', function(req, res, next){
  // 요청 body에 전송된 정보를 기준으로 user객체 데이터 생성
  const user = new User({
    name: req.body.name,
    age: req.body.age,
    married: req.body.married,
  });
  user.save()
  .then((result)=>{
    console.log(result);
    res.status(201).json(result);
  })
  .catch((err)=>{
    console.error(err);
    next(err);
  });
});

module.exports = router;
  • User.find({}) : User테이블 전체 조회
  • user.save() : 생성한 user데이터 추가

2) Comments

유형 라우트 설명
GET /comments/:id id 사용자의 댓글 목록 조회
POST /comments 댓글 추가
PATCH /comments/:id ObjectId가 일치하는 댓글 수정
DELETE /comments/:id ObjectId가 일치하는 댓글 삭제

/routes/comments.js

var express = require('express');
var Comment = require('../schemas/comment');

var router = express.Router();

router.get('/:id', function (req, res, next) {
  Comment.find({ commenter: req.params.id }).populate('commenter')
    .then((comments) => {
      console.log(comments);
      res.json(comments);
    })
    .catch((err) => {
      console.error(err);
      next(err);
    });
});

router.post('/', function (req, res, next) {
  const comment = new Comment({
    commenter: req.body.id,
    comment: req.body.comment,
  });
  comment.save()
    .then((result) => {
      return Comment.populate(result, { path: 'commenter' });
    })
    .then((result) => {
      res.status(201).json(result);
    })
    .catch((err) => {
      console.error(err);
      next(err);
    });
});

router.patch('/:id', function (req, res, next) {
  Comment.update({ _id: req.params.id }, { comment: req.body.comment })
    .then((result) => {
      res.json(result);
    })
    .catch((err) => {
      console.error(err);
      next(err);
    });
});

router.delete('/:id', function (req, res, next) {
  Comment.remove({ _id: req.params.id })
    .then((result) => {
      res.json(result);
    })
    .catch((err) => {
      console.error(err);
      next(err);
    });
});

module.exports = router;
  • populate(): comment다큐먼트에서 user다큐먼트의 ObjectId를 참조하고 있는데 이를 실제 객체로 치환해 조회하게 해줍니다.

3) routes/index.js

기본으로 생성되는 index.js에서는 기본 index 뷰를 랜더링하도록 구현되어 있습니다.

우리가 원하는 페이지를 랜더링하고 사용자에 대한 정보도 같이 전달해 랜더링하도록 고쳐봅니다.

var express = require('express');
var User = require('../schemas/user');

var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  User.find({})
  .then((users)=>{
    res.render('mongoose',{ users });
  })
  .catch((err)=>{
    console.error(err);
    next(err);
  });
});

module.exports = router;

mongoose라는 뷰를 만들어 줄 겁니다.

2. View 만들기

구현 목표 화면

 

위와 같이 전체 사용자들을 조회, 등록할 수 있고, 사용자를 누르면 해당 사용자의 댓글 목록을 볼 수 있는 뷰를 만듭니다.

/views/mongoose.pug

doctype html
html
  head
    meta(charset='utf-8')
    title 몽구스 서버
    style.
        table{
            border: 1px solid black;
            border-collapse: collapse;
        }
        table th, table td{
            border: 1px solid black;
        }
    body
      div
        form#user-form
          fieldset
            legend 사용자 등록
            div
              input#username(type="text" placeholder="이름")
            div
              input#age(type="number" placeholder="나이")
            div
              input#married(type="checkbox")
              label(for="married") 결혼여부
            button(type="submit") 등록
        br
        table#user-list
          thead
            tr
              th 아이디
              th 이름
              th 나이
              th 결혼여부
          tbody
            for user in users
              tr
                td= user._id
                td= user.name
                td= user.age
                td= user.married ? '기혼' : '미혼'
        br
        div
          form#comment-form
            fieldset
              legend 댓글 등록
              div
                input#userid(type="text" placeholder="사용자 아이디")
              div
                input#comment(type="text" placeholder="댓글")
              button(type="submit") 등록
        br
        table#comment-list
          thead
            tr
              th 아이디
              th 작성자
              th 댓글
              th 수정
              th 삭제
          tbody
        script(src='/mongoose.js')

서버가 구동될 때 참조하는 정적인 파일들은 public 디렉토리에 담기게 됩니다.

위의 페이지에서 동적으로 동작하는 부분들을 /public/mongoose.js 파일로 작성합니다.

/public/mongoose.js

// 사용자 이름 눌렀을 때 댓글 로딩
document.querySelectorAll('#user-list tr').forEach(function (el) {
    el.addEventListener('click', function () {
      var id = el.querySelector('td').textContent;
      getComment(id);
    });
  });
  // 사용자 로딩
  function getUser() {
    var xhr = new XMLHttpRequest();
    xhr.onload = function () {
      if (xhr.status === 200) {
        var users = JSON.parse(xhr.responseText);
        console.log(users);
        var tbody = document.querySelector('#user-list tbody');
        tbody.innerHTML = '';
        users.map(function (user) {
          var row = document.createElement('tr');
          row.addEventListener('click', function () {
            getComment(user._id);
          });
          var td = document.createElement('td');
          td.textContent = user._id;
          row.appendChild(td);
          td = document.createElement('td');
          td.textContent = user.name;
          row.appendChild(td);
          td = document.createElement('td');
          td.textContent = user.age;
          row.appendChild(td);
          td = document.createElement('td');
          td.textContent = user.married ? '기혼' : '미혼';
          row.appendChild(td);
          tbody.appendChild(row);
        });
      } else {
        console.error(xhr.responseText);
      }
    };
    xhr.open('GET', '/users');
    xhr.send();
  }
  // 댓글 로딩
  function getComment(id) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function () {
      if (xhr.status === 200) {
        var comments = JSON.parse(xhr.responseText);
        var tbody = document.querySelector('#comment-list tbody');
        tbody.innerHTML = '';
        comments.map(function (comment) {
          var row = document.createElement('tr');
          var td = document.createElement('td');
          td.textContent = comment._id;
          row.appendChild(td);
          td = document.createElement('td');
          td.textContent = comment.commenter.name;
          row.appendChild(td);
          td = document.createElement('td');
          td.textContent = comment.comment;
          row.appendChild(td);
          var edit = document.createElement('button');
          edit.textContent = '수정';
          edit.addEventListener('click', function () { // 수정 클릭 시
            var newComment = prompt('바꿀 내용을 입력하세요');
            if (!newComment) {
              return alert('내용을 반드시 입력하셔야 합니다');
            }
            var xhr = new XMLHttpRequest();
            xhr.onload = function () {
              if (xhr.status === 200) {
                console.log(xhr.responseText);
                getComment(id);
              } else {
                console.error(xhr.responseText);
              }
            };
            xhr.open('PATCH', '/comments/' + comment._id);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify({ comment: newComment }));
          });
          var remove = document.createElement('button');
          remove.textContent = '삭제';
          remove.addEventListener('click', function () { // 삭제 클릭 시
            var xhr = new XMLHttpRequest();
            xhr.onload = function () {
              if (xhr.status === 200) {
                console.log(xhr.responseText);
                getComment(id);
              } else {
                console.error(xhr.responseText);
              }
            };
            xhr.open('DELETE', '/comments/' + comment._id);
            xhr.send();
          });
          td = document.createElement('td');
          td.appendChild(edit);
          row.appendChild(td);
          td = document.createElement('td');
          td.appendChild(remove);
          row.appendChild(td);
          tbody.appendChild(row);
        });
      } else {
        console.error(xhr.responseText);
      }
    };
    xhr.open('GET', '/comments/' + id);
    xhr.send();
  }
  // 사용자 등록 시
  document.getElementById('user-form').addEventListener('submit', function (e) {
    e.preventDefault();
    var name = e.target.username.value;
    var age = e.target.age.value;
    var married = e.target.married.checked;
    if (!name) {
      return alert('이름을 입력하세요');
    }
    if (!age) {
      return alert('나이를 입력하세요');
    }
    var xhr = new XMLHttpRequest();
    xhr.onload = function () {
      if (xhr.status === 201) {
        console.log(xhr.responseText);
        getUser();
      } else {
        console.error(xhr.responseText);
      }
    };
    xhr.open('POST', '/users');
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify({ name: name, age: age, married: married }));
    e.target.username.value = '';
    e.target.age.value = '';
    e.target.married.checked = false;
  });
  // 댓글 등록 시
  document.getElementById('comment-form').addEventListener('submit', function (e) {
    e.preventDefault();
    var id = e.target.userid.value;
    var comment = e.target.comment.value;
    if (!id) {
      return alert('아이디를 입력하세요');
    }
    if (!comment) {
      return alert('댓글을 입력하세요');
    }
    var xhr = new XMLHttpRequest();
    xhr.onload = function () {
      if (xhr.status === 201) {
        console.log(xhr.responseText);
        getComment(id);
      } else {
        console.error(xhr.responseText);
      }
    };
    xhr.open('POST', '/comments');
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify({ id: id, comment: comment }));
    e.target.userid.value = '';
    e.target.comment.value = '';
  });

테이블의 사용자가 눌렸을 때 해당 사용자의 comments 정보를 로드합니다.

해당 코멘트 정보를 담음 table 요소를 추가할 때 수정, 삭제 버튼이 담기게 됩니다.

각 버튼들에 수정, 삭제 기능을 수행하기 위핸 이벤트리스너를 연결해둡니다.

이와 별개로 사용자 등록, 댓글 등록 기능을 구현합니다.

 

http request를 위해 XMLHttpRequest를 활용합니다.

3. 결과 확인하기

간단하게 Express와 MongoDB를 연동한 웹을 구현해 보았습니다.

 

해당 프로젝트 루트 디렉토리에서 서버를 구동해봅니다.

$ npm start

서버가 정상적으로 동작하면 http://localhost:3000에 접속해 결과를 확인해 볼 수 있습니다.


참조

- (MongoDB) Mongoose populate