주소록 - Show, Edit, Update, Destroy

소스코드

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

이 게시물의 소스코드는 주소록 만들기 / 주소록 - Index, New, Create에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 26718b1
git reset --soft 9461bc7
npm install
atom .

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

git clone https://github.com/a-mean-blogger/contact-book.git
cd contact-book
git reset --hard 26718b1
git reset --soft 9461bc7
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/contact-book/tree/26718b12b4855ed227774d94267bcb37ed477eb9


7 actions 중 나머지 show, edit, update, destroy를 구현해 봅시다.

전체 목록(index)에서 하나를 선택하면 해당 데이터를 보여주고(show)
해당 데이터를 수정할 수 있는 form을 만들어서 이 정보를 서버로 전달할 수 있게 하고(edit)
서버가 이 정보를 사용해서 DB에 정보를 수정하고(update)
해당 데이터를 삭제할 수도 있습니다(destroy).

폴더구조

주황색은 변경된 파일, 녹색은 새로 생성된 파일, 회색은 변화가 없는 파일입니다.

Package 설치

7 actions 중 update과 destroy는 HTTP Methods 중 put과 delete을 사용하는데, 대부분의 브라우저의 form은 get과 post 만을 허용하고 나머지는 허용하지 않습니다. 브라우저에서 허용하진 않지만 나중에 API로 연결할 때는 문제가 없기 때문에 HTTP를 올바르게 사용하는 법을 익히는 것이 더 중요합니다. 지금은 일단 method override라는  package를 설치하여 이를 우회하도록하겠습니다.

$ npm install --save method-override

이 package는 query로 method 값을 받아서 request의 HTTP method를 바꿔주는 역할을 합니다.

해당 부분은 코드에서 살펴보겠습니다.

코드

var express = require("express");
var mongoose = require("mongoose");
var bodyParser = require("body-parser");
var methodOverride = require("method-override"); // 1 
var app = express();

// DB setting ...

// Other settings
app.set("view engine", "ejs");
app.use(express.static(__dirname+"/public"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
app.use(methodOverride("_method")); // 2 

// DB schema ...

// Routes
// Home ...
// Contacts - Index ...
// Contacts - New ...
// Contacts - create ...
// Contacts - show // 3 
app.get("/contacts/:id", function(req, res){
  Contact.findOne({_id:req.params.id}, function(err, contact){
    if(err) return res.json(err);
    res.render("contacts/show", {contact:contact});  });
});
// Contacts - edit // 4 
app.get("/contacts/:id/edit", function(req, res){
  Contact.findOne({_id:req.params.id}, function(err, contact){
    if(err) return res.json(err);    res.render("contacts/edit", {contact:contact});  });
});
// Contacts - update // 5 
app.put("/contacts/:id", function(req, res){
  Contact.findOneAndUpdate({_id:req.params.id}, req.body, function(err, contact){
    if(err) return res.json(err);    res.redirect("/contacts/"+req.params.id);  });
});
// Contacts - destroy // 6
app.deleteOne("/contacts/:id", function(req, res){
  Contact.remove({_id:req.params.id}, function(err, contact){
    if(err) return res.json(err);    res.redirect("/contacts");  });
});


// Port setting ...

...으로 표시된 부분은 이전과 변화가 없는 부분입니다.

새로 추가된 부분들을 각각 살펴봅시다.

var methodOverride = require("method-override"); // 1 

1. method-override module을 methodOverride변수에 담습니다.

app.use(methodOverride("_method")); // 2 

2. _method의 query로 들어오는 값으로 HTTP method를 바꿉니다. 예를들어 http://example.com/category/id?_method=delete를 받으면 _method의 값인 delete을 읽어 해당 request의 HTTP method를 delete으로 바꿉니다.

// Contacts - show // 3 
app.get("/contacts/:id", function(req, res){
  Contact.findOne({_id:req.params.id}, function(err, contact){
    if(err) return res.json(err);    res.render("contacts/show", {contact:contact});  });
});

3. "contacts/:id"에 get 요청이 오는 경우 :

:id처럼 route에 콜론(:)을 사용하면 해당 위치의 값을 받아 req.params에 넣게 됩니다. 예를 들어 "contacts/abcd1234"가 입력되면 "contacts/:id" route에서 이를 받아 req.params.id에 "abcd1234"를 넣게 됩니다.

Model.findOne은 DB에서 해당 model의 document를 하나 찾는 함수입니다. 첫번째 parameter로 찾을 조건을 object로 입력하고 data를 찾은 후 콜백 함수를 호출합니다. Model.find와 비교해서 Model.find는 조건에 맞는 결과를 모두 찾아 array로 전달하는데 비해 Model.findOne은 조건에 맞는 결과를 하나 찾아 object로 전달합니다. (검색 결과가 없다면 null이 전달됩니다.)

위 경우에는 {_id:req.params.id}를 조건으로 전달하고 있는데, 즉 DB의 contacts collection에서 _id가 req.params.id와 일치하는 data를 찾는 조건입니다.

에러가 없다면 검색 결과를 받아 views/contacts/show.ejs를 render합니다.

// Contacts - edit // 4 
app.get("/contacts/:id/edit", function(req, res){
  Contact.findOne({_id:req.params.id}, function(err, contact){
    if(err) return res.json(err);    res.render("contacts/edit", {contact:contact});  });
});

4. "contacts/:id/edit"에 get 요청이 오는 경우 :

Model.findOne이 다시 사용되었습니다. 검색 결과를 받아 views/contacts/edit.ejs를 render합니다.

// Contacts - update // 5 
app.put("/contacts/:id", function(req, res){
  Contact.findOneAndUpdate({_id:req.params.id}, req.body, function(err, contact){
    if(err) return res.json(err);    res.redirect("/contacts/"+req.params.id);  });
});

5. "contacts/:id"에 put 요청이 오는 경우 :

Model.findOneAndUpdate는 DB에서 해당 model의 document를 하나 찾아 그 data를 수정하는 함수입니다. 첫번째 parameter로 찾을 조건을 object로 입력하고 두번째 parameter로 update할 정보를 object로 입력data를 찾은 후 callback함수를 호출합니다.
이때 callback함수로 넘겨지는 값은 수정되기 전의 값입니다. 만약 업데이트 된 후의 값을 보고 싶다면 콜백 함수 전에 parameter로 {new:true}를 넣어주면 됩니다.

Data 수정 후 "/contacts/"+req.params.id로 redirect합니다.

// Contacts - destroy // 6
app.delete("/contacts/:id", function(req, res){
  Contact.deleteOne({_id:req.params.id}, function(err){
    if(err) return res.json(err);    res.redirect("/contacts");  });
});

6. "contacts/:id"에 delete 요청이 오는 경우 :

Model.deleteOne은 DB에서 해당 model의 document를 하나 찾아 삭제하는 함수입니다. 첫번째 parameter로 찾을 조건을 object로 입력하고 data를 찾은 후 callback함수를 호출합니다.

Data 삭제후 "/contacts"로 redirect합니다.

다음으로 view 파일들을 살펴봅시다.

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

<!DOCTYPE html>
<html>
  <head>
    <% include ../partials/head %>
  </head>
  <body>
    <% include ../partials/nav %>

    <div class="contact contact-index">
      <h2>Index</h2>
      <ul>
        <% contacts.forEach(function(contact) { %>
          <li>
            <a href="/contacts/<%= contact._id %>"><%= contact.name %></a> <!-- 1 -->
          </li>
        <% }) %>
      </ul>
    </div>
  </body>
</html>

1. 이름에 a tag를 추가하여 show action과 연결했습니다.

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

<!DOCTYPE html>
<html>
  <head>
    <% include ../partials/head %>
  </head>
  <body>
    <% include ../partials/nav %>

    <div class="contact contact-show">
      <div class="contact-menu">
        <a href="/contacts">Back</a>
        <a href="/contacts/<%= contact._id %>/edit">Edit</a> <!-- 1 -->
        <form action="/contacts/<%= contact._id %>?_method=delete" method="post"> <!-- 2 -->
          <a href="#" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a>
        </form>
      <div>
      <h2>Show</h2>
      <h3><%= contact.name%></h3>
      <div>
        <label>Email</label><span><%= contact.email %></span>
      </div>
      <div>
        <label>Phone</label><span><%= contact.phone %></span>
      </div>
    </div>
  </body>
</html>

1. a tag로 Edit을 만들어 edit action에 연결했습니다.

2. a tag로는 get만 요청할 수 있기 때문에 Delete은 form으로 처리해야 합니다. form에서도 post밖에 요청할 수 없기 때문에 ?_method=delete이 action에 query로 추가되었습니다. 이 부분은 서버의 method override package에 의해 처리되어 delete 요청으로 변경됩니다.

3. Delete을 눌렀을때 onclick을 사용해서 confirm을 띄우고 yes인 경우 form을 submit하게 합니다.

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

<!DOCTYPE html>
<html>
  <head>
    <% include ../partials/head %>
  </head>
  <body>
    <% include ../partials/nav %>

    <div class="contact contact-edit">
      <div class="contact-menu">
        <a href="/contacts/<%= contact._id %>">Back</a>
      </div>
      <h2>Edit</h2>
      <form class="contact-form" action="/contacts/<%= contact._id %>?_method=put" method="post">
        <div>
          <label for="name">Name</label>
          <input type="text" id="name" name="name" value="<%= contact.name %>">
        </div>
        <div>
          <label for="email">Email</label>
          <input type="text" id="email" name="email" value="<%= contact.email %>">
        </div>
        <div>
          <label for="phone">Phone</label>
          <input type="text" id="phone" name="phone" value="<%= contact.phone %>">
        </div>
        <div>
          <button type="submit">Submit</button>
        </div>
      </div>
    </form>
  </body>
</html>

edit.ejs는 new.ejs와 거의 같지만 form에 해당 data의 값들이 미리 들어 있고, form action이 다릅니다.

put 역시 form에서 사용할 수 없으므로 _method=put을 사용하였습니다.

/* public/css/master.css */

.contact h2{
  color: tomato;
}
.contact .contact-form label,
.contact-show label{
  display: inline-block;
  width: 50px;
}
.contact .contact-menu form{
  display: inline-block;
}

CSS 는 따로 설명하지 않겠습니다.

실행결과

nodemon으로 실행결과를 확인합시다.

$ nodemon

contacts/new로 들어가면 데이터를 생성할 수 있는 form이 나옵니다.

새로운 데이터를 생성한 후 submit 합니다.

test가 생성되었습니다.

test를 클릭하면 Edit과 Delete이 새로 생긴 것이 보입니다.

Edit을 클릭하면 값을 수정할 수 있는 form이 나옵니다.

값을 수정한 후 submit합니다.

값이 변경되었습니다.

Delete을 눌러봅시다.

경고가 보입니다. Ok를 누릅니다.

값이 지워졌습니다.

마치며..

마침내 CRUD가 모두 되는 주소록의 기능이 완성되었습니다.
index.js 파일에 제법 코드가 많아졌는데, 다음 강의에서는 코드가 더 복잡해지기 전에 Node.js Module을 사용하여 코드를 정리하는 법을 알아보겠습니다.

댓글

Deleted Comment
Deleted Comment
s
seung joo Rim (Jeremy) 2017.04.03
사소한 오타지만 index.html이 아니라 index.ejs 으로 저장해야 실행이 되네요^^  index부분 첫줄 주석부분에 오타입니다^^ 잘보고 있습니다.
I
Ian H 2017.04.03
@seung joo Rim (Jeremy),
앗, 수정했습니다. 알려주셔서 감사합니다^^
정석호 2017.07.03
method override 모듈을 쓰지 않고 클라이언트단에서 ajax 로 메소드 설정해줘서 통신해도 문제 없나요??
I
Ian H 2017.07.03
@정석호,
ajax를 쓰시면 method override 안쓰셔도 됩니다^^
정석호 2017.07.04
@Ian H,
감사합니당
Y
Ye Lyn Kim 2017.07.17
죄송합니다만ㅠㅠ TypeError: Cannot read property 'id' of undefined     at C:\workspace\contact-book\index.js:85:43와 같은 에러는 왜 발생하는것인지 궁금합니다. 그대로 했는데 잘 안되네요ㅠㅠ
I
Ian H 2017.07.17
@Ye Lyn Kim,
소스코드를 봐야 알 수 있을 것 같은데요,  https://github.com/a-mean-blogger/contact-book/blob/bc0cc4993e1f3c117619e484a5485a5140be12e6/index.js 제 코드의 index.js 85번째 줄에는 'id'가 없어서..
현재 소스코드를 github에 올리시고 저에게 주소를 알려주시면 살펴보겠습니다.
정영호 2017.07.17
질문이 있습니다! form 부분에서 보안상의 이유로 get과 post만 사용이 가능하다고 되어있는데 method-override를 사용하면 그 보안을 억지로 깨는 것(?) 아닌가요??
I
Ian H 2017.07.17
@정영호,
제가 배울때 보안상의 이유라고 들었는데, 사실 더 찾아보니까 보안상의 이유라기 보다 표준을 만들 때 form은 post와 get만 허용해야한다 vs form이 나머지 method도 허용해야 한다는 논란이 있었는데 결국 form은 post와 get만 허용하는 걸로 결정이 났고, 사람들은 계속 나머지도 허용해야 한다고 주장하고 있는 상태네요.. 출처: https://softwareengineering.stackexchange.com/questions/114156/why-are-there-are-no-put-and-delete-methods-on-html-forms 본문 내용도 수정을 하였습니다.
C
Chooni Jo 2018.08.10
사소하지만, "contracts/new로 들어가면 데이터를 생성할 수 있는 form이 나옵니다." 에 오타가 있습니다. contacts/new 로 정정하시면 될 것 같습니다~!!
I
Ian H 2018.08.10
@Chooni Jo,
알려주셔서 감사합니다^^ 수정하였습니다!
즐기면서공부하자 2018.08.21
정말재밌네욤 ㅋㅋㅋ 좋은 강의 감사합니다~ 꾸벅.
I
Ian H 2018.08.21
@즐기면서공부하자,
와 재미있으면 공부도 더 잘되죠. 화이팅!
댓글쓰기

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

UP