[Node.JS] 강좌 11편: Express와 Mongoose를 통해 MongoDB와 연동하여 RESTful API 만들기


list

이번 강좌에서는 Mongoose 를 통하여 Node.js 에서 MongoDB와 연동하는것을 배워보겠습니다.

1. 소개

Mongoose는 MongoDB 기반 ODM(Object Data Mapping) Node.JS 전용 라이브러리입니다. ODM은 데이터베이스와 객체지향 프로그래밍 언어 사이 호환되지 않는 데이터를 변환하는 프로그래밍 기법입니다. 즉 MongoDB 에 있는 데이터를 여러분의 Application에서 JavaScript 객체로 사용 할 수 있도록 해줍니다.

Note: 이 강좌는 Node.jsMongoDB가 설치되있다는 전제하에 진행됩니다.
또한, MongoDB에 전반적인 지식이 없다면 mongoose 사용이 다소 어려울 수 있습니다.
MongoDB 기초 강좌는 여기서 볼 수 있습니다.

2. 프로젝트 생성 및 패키지 설치

Mongoose 를 배워가면서, 간단한 Express 기반의 RESTful 프로젝트를 만들어보도록 하겠습니다.

2.1 프로젝트 생성

우선 npm init 을 통하여 package.json 을 생성하세요. 엔터를 계속 눌러 설정값은 기본값으로 하시면 됩니다.

$ npm init

2.2 패키지 설치

프로젝트에서 사용 할 패키지를 설치하겠습니다.

  1. express: 웹프레임워크
  2. body-parser: 데이터 처리 미들웨어
  3. mongoose: MongoDB 연동 라이브러리
$ npm install --save express mongoose body-parser

명령어를 입력하시면 자동으로 패키지를 설치하고, package.json 파일에 패키지 리스트를 추가합니다.

 

3. 서버 설정하기

3.1 디렉토리 구조

먼저 저희가 만들 프로젝트의 디렉토리 구조를 살펴봅시다.

- models/
----- book.js
- node_modules/
- routes
----- index.js
- app.js
- package.json

이 파일들은 강좌를 진행하면서 만들도록 하겠습니다.

 

3.2 Express 로 이용한 웹서버 생성

mongoose를 사용하기 위해서 우선 book 데이터를 조회·수정·삭제 하는간단한 RESTful 웹서버를 작성해보겠습니다.

이 서버에 만들 API 목록은 다음과 같습니다.

ROUTE METHOD DESCRIPTION
/api/books GET 모든 book 데이터 조회
/api/books/:book_id GET  _id 값으로 데이터 조회
/api/books/author/:author GET author 값으로 데이터 조회
/api/books POST book 데이터 생성
/api/books/:book_id PUT book 데이터 수정
/api/books/:book_id DELETE book 데이터 제거

 

우선 서버의 메인 파일인 app.js를 작성하세요.

// app.js

// [LOAD PACKAGES]
var express     = require('express');
var app         = express();
var bodyParser  = require('body-parser');
var mongoose    = require('mongoose');

// [CONFIGURE APP TO USE bodyParser]
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// [CONFIGURE SERVER PORT]
var port = process.env.PORT || 8080;

// [CONFIGURE ROUTER]
var router = require('./routes')(app)

// [RUN SERVER]
var server = app.listen(port, function(){
 console.log("Express server has started on port " + port)
});

line 17 에서 router 모듈을 불러오게 했죠? 이제 router를 작성해봅시다.

routes/index.js 에 다음 코드를 입력하세요.

// routes/index.js

module.exports = function(app)
{
    // GET ALL BOOKS
    app.get('/api/books', function(req,res){
        res.end();
    });

    // GET SINGLE BOOK
    app.get('/api/books/:book_id', function(req, res){
        res.end();
    });

    // GET BOOK BY AUTHOR
    app.get('/api/books/author/:author', function(req, res){
        res.end();
    });

    // CREATE BOOK
    app.post('/api/books', function(req, res){
        res.end();
    });

    // UPDATE THE BOOK
    app.put('/api/books/:book_id', function(req, res){
        res.end();
    });

    // DELETE BOOK
    app.delete('/api/books/:book_id', function(req, res){
        res.end();
    });

}

 

4. MongoDB 서버 연결

MongoDB 서버에 연결 하는 방법은 다음과 같습니다.

// app.js

// ......

var mongoose    = require('mongoose');

// ......

// [ CONFIGURE mongoose ]

// CONNECT TO MONGODB SERVER
var db = mongoose.connection;
db.on('error', console.error);
db.once('open', function(){
    // CONNECTED TO MONGODB SERVER
    console.log("Connected to mongod server");
});

mongoose.connect('mongodb://localhost/mongodb_tutorial');

// ......

mongoose.connect() 메소드로 서버에 접속을 할 수 있으며, 따로 설정 할 파라미터가 있다면 다음과 같이 uri를 설정하면 됩니다.

mongoose.connect('mongodb://username:password@host:port/database?options...');

이 강좌에서는 mongodb_tutorial db를 사용하도록 하겠습니다.

 

5. Schema & Model

5.1 schema

schema는 document의 구조가 어떻게 생겼는지 알려주는 역할을 합니다.

schema를 만드는 방법은 다음과 같습니다. 이 코드를 models/book.js 에 입력하세요.

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var bookSchema = new Schema({
    title: String,
    author: String,
    published_date: { type: Date, default: Date.now  }
});

module.exports = mongoose.model('book', bookSchema);

schema에서 사용되는 SchemaType은 총 8종류가 있습니다.

  1. String
  2. Number
  3. Date
  4. Buffer
  5. Boolean
  6. Mixed
  7. Objectid
  8. Array

이를 사용하는 예제는 매뉴얼을 참고하세요.

5.2 model

model은 데이터베이스에서 데이터를 읽고, 생성하고, 수정하는프로그래밍 인터페이스를 정의합니다.

// DEFINE MODEL
var Book = mongoose.model('book', bookSchema);

첫번째 인자 book’ 은 해당 document가 사용 할 collection의 단수적 표현입니다. 즉, 이 모델에서는 ‘books’ collection 을 사용하게 되겠죠. 이렇게 자동으로 단수적 표현을 복수적(plural) 형태로 변환하여 그걸 collection 이름으로 사용합니다. collection 이름을 plural 형태로 사용하는건 mongodb의 네이밍컨벤션 중 하나입니다.

만약에, collection 이름을 임의로 정하고 싶다면, schema 를 만들 때 따로 설정해야 합니다.

var dataSchema = new Schema({..}, { collection: 'COLLECTION_NAME' });

 

model을 만들고 나면, model 을 사용하여 다음과 같이 데이터를 데이터베이스에 저장하거나 조회 할 수 있습니다.

var book = new Book({
    name: "NodeJS Tutorial",
    author: "velopert"
});
book.save(function(err, book){
    if(err) return console.error(err);
    console.dir(book);
});

이와 같이 model을 사용하여 데이터베이스와 연동하는 자세한 방법에 대해서는 다음 섹션에서 다루도록 하겠습니다.

저희는 model을 models/bear.js 를 모듈화해서 만들게 할 것이므로, 다음과 같이 해당파일의 마지막줄에서 model을 모듈화하세요.

// models/book.js

var Schema = mongoose.Schema;

var bookSchema = new Schema({
    title: String,
    author: String,
    published_date: { type: Date, default: Date.now  }
});

module.exports = mongoose.model('book', bookSchema);

app.js에서 이 모듈을 불러와보겠습니다.

// app.js

// ...

// CONNECT TO MONGODB SERVER

// ...

// DEFINE MODEL
var Book = require('./models/book');

// ...

 

6. CRUD (Create, Retrieve, Update, Delete)

6.0 시작하기 전에..

3번 섹션에서 만들었던 API 를 직접 구현해가면서 데이터를 생성/조회/수정/제거 하는 방법을 알아보겠습니다.|
라우터에서 Book 모델을 사용해야 하므로, 라우터에 Book을 전달해줘야겠죠?
따라서 /routes/index.jsapp.js 를 우선 수정해야합니다.

// routes/index.js
modules.exports = function(app, Book)
{
    // ....
}
// app.js

// ...

var router = require('./routes')(app, Book);

// ...

6.1 Create ( POST /api/books )

book 데이터를 데이터베이스에 저장하는 API 입니다.

app.post('/api/books', function(req, res){
    var book = new Book();
    book.title = req.body.name;
    book.author = req.body.author;
    book.published_date = new Date(req.body.published_date);

    book.save(function(err){
        if(err){
            console.error(err);
            res.json({result: 0});
            return;
        }

        res.json({result: 1});

    });
});

.save 메소드는 데이터를 데이터베이스에 저장합니다. err 을 통하여 오류처리가 가능합니다.

이 API 에서는 데이터 저장에 성공하면 result: 1을, 실패하면 result: 0 을 반환합니다.

이미지 26

(REST API 테스팅에 사용된 어플리케이션은 크롬 확장 프로그램 Insomnia 입니다)


6.2.1 RETRIEVE ( GET /api/books )

모든 book 데이터를 조회하는 API 입니다.

// GET ALL BOOKS
app.get('/api/books', function(req,res){
    Book.find(function(err, books){
        if(err) return res.status(500).send({error: 'database failure'});
        res.json(books);
    })
});

데이터를 조회 할 때는 find() 메소드가 사용됩니다.
query를 파라미터 값으로 전달 할 수 있으며, 파라미터가 없을 시, 모든 데이터를 조회합니다.

데이터베이스에 오류가 발생하면 HTTP Status 500 과 함께 에러를 출력합니다.

이미지 25


6.2.2 RETRIEVE ( GET /api/books/:book_id )

데이터베이스에서 Id 값을 찾아서 book document를 출력합니다.

// GET SINGLE BOOK
app.get('/api/books/:book_id', function(req, res){
    Book.findOne({_id: req.params.book_id}, function(err, book){
        if(err) return res.status(500).json({error: err});
        if(!book) return res.status(404).json({error: 'book not found'});
        res.json(book);
    })
});

하나의 데이터만 찾을 것이기 때문에, findOne 메소드가 사용되었습니다.

오류가 발생하면 500, 데이터가 없으면 404 HTTP Status 와 함께 오류를 출력합니다.
이미지 30


6.2.3 RETRIEVE ( GET /api/books/author/:author )

author 값이 매칭되는 데이터를 찾아 출력합니다.

// GET BOOKS BY AUTHOR
app.get('/api/books/author/:author', function(req, res){
    Book.find({author: req.params.author}, {_id: 0, title: 1, published_date: 1},  function(err, books){
        if(err) return res.status(500).json({error: err});
        if(books.length === 0) return res.status(404).json({error: 'book not found'});
        res.json(books);
    })
});

find() 메소드에서 첫번째 인자에는 query 를, 두번째는 projection 을 전달해주었습니다.
이를 통하여 author 값으로 찾아서 title 과 published_date 만 출력합니다.
(만약에 projection이 생략되었다면 모든 field 를 출력합니다.)

이미지 33

 


6.3 UPDATE ( PUT /api/books/:book_id )

book_id 를 찾아서 document를 수정합니다.

// UPDATE THE BOOK
app.put('/api/books/:book_id', function(req, res){
    Book.findById(req.params.book_id, function(err, book){
        if(err) return res.status(500).json({ error: 'database failure' });
        if(!book) return res.status(404).json({ error: 'book not found' });

        if(req.body.title) book.title = req.body.title;
        if(req.body.author) book.author = req.body.author;
        if(req.body.published_date) book.published_date = req.body.published_date;

        book.save(function(err){
            if(err) res.status(500).json({error: 'failed to update'});
            res.json({message: 'book updated'});
        });

    });

});

데이터를 수정 할 땐, 데이터를 먼저 찾은 후, save() 메소드를 통하여 수정하면 됩니다.
update하는 방법은 이 외에도 다른 방법이 있는데요, 만약 어플리케이션에서 기존 document를 굳이 조회 할 필요가없다면
update() 메소드를 통하여 바로 document를 업데이트 할 수 있습니다.
아래 코드는 코드와 같은 동작을 하지만 업데이트하는 과정에서 document를 조회 하지 않습니다.

// UPDATE THE BOOK (ALTERNATIVE)
app.put('/api/books/:book_id', function(req, res){
    Book.update({ _id: req.params.book_id }, { $set: req.body }, function(err, output){
        if(err) res.status(500).json({ error: 'database failure' });
        console.log(output);
        if(!output.n) return res.status(404).json({ error: 'book not found' });
        res.json( { message: 'book updated' } );
    })
});

여기서 output 은 mongod 에서 출력하는 결과물입니다.

{ 
    ok: 1, 
    nModified: 0,
    n: 1
}

여기서 nModified는 변경한 document 갯수, n은 select된 document 갯수입니다.
update() 를 실행하였을 떄, 기존 내용이 업데이트 할 내용과 같으면 nModified 는 0 으로 되기 때문에,
n 값을 비교하여 성공여부를 판단합니다.

이미지 47


6.4 DELETE ( /api/books/:book_id )

book_id를 찾아서 document를 제거합니다.

// DELETE BOOK
app.delete('/api/books/:book_id', function(req, res){
    Book.remove({ _id: req.params.book_id }, function(err, output){
        if(err) return res.status(500).json({ error: "database failure" });

        /* ( SINCE DELETE OPERATION IS IDEMPOTENT, NO NEED TO SPECIFY )
        if(!output.result.n) return res.status(404).json({ error: "book not found" });
        res.json({ message: "book deleted" });
        */

        res.status(204).end();
    })
});

document를 제거 할 땐 remove() 메소드가 사용됩니다.

DELETE 는 idempotent(어떤 과정을 몇번이고 반복 수행 하여도 결과가 동일함; 즉 삭제한 데이터를 삭제하더라도, 존재하지 않는 다큐먼트를 제거 시도를하더라도 달라질게 없음) 하므로, 성공하였을떄나 실패하였을때나 결과값이 같습니다. 여기서 204 HTTP status 는 No Content 로서, 요청한 작업을 수행하였고 데이터를 반환 할 필요가 없다는것을 의미합니다.6~9 번줄은 실제로 존재하는 데이터를 삭제하였는지 확인해주는 코드이나, 그럴 필요가 없으므로 주석처리되었습니다.

이미지 48


마치면서..

이 강좌에서 사용된 프로젝트는 깃헙 (/velopert/mongoose_tutorial) 에 업로드되어있습니다. 테스트 하고싶으신 분은 참조하세요.

 

다음 강좌에서는 gulp를 다뤄보도록하겠습니다.

list

  • Woonyeol Lee

    안녕하세요. 후반부에 mongodb_tutorial 테스트 하는 GUI 프로그램 이름은 무엇인가요?
    저도 API 를 만들어서 테스트 해보고 싶은데 해 볼 수 있는 방법이 없네요..

    • Insomnia 라는 크롬확장프로그램입니다.
      게시물 자세히 보시면 링크 있습니다 🙂

      • Woonyeol Lee

        아 있네요! 죄송합니다. 급해서 제가 미처 보지 못한거 같습니다.
        빠른 답변 감사합니다.

  • 웅이아버지

    mongoerror 에러가 자꾸 나는 이유를 모르겠슴다..nodejs 싹 삭제 하고 다시 해봐도 에러 나네용..

  • 웅이아버지

    아..먼저 몽고DB를 설치 했어야 되는군여 그것도 모르고 한참을 삽질..

    • ㅎㅎㅎ 여유를 갖고 천천히 공부를 하길 바라요~
      이 강좌 이후로는 Nodejs 강좌를 준비한게 없는데..

      요즘은 제가 React.js 를 파고있습니다.
      시간이 있으시다면 React.js 공부해보시는거 추천드려요 !

  • 저장

    post명령 주소를 돌려도 데이터베이스에 데이터가 들어가지 않길래
    같은 /api/books에 post랑 get 명령이 둘다있어 그런가하고 get코드를 무효화해 돌리기도 했지만
    데이터가 저장이 안되네요ㅠㅠ 데이터저장 어떻게하는거죠??
    안에 들어있는 데이터가 없으니 [] 빈 배열만 호출됩니다…

    • 감사합니다

      헉 그래서 get으로 save명령 쓰니깐 데이터로 잘 들어가네요…. post로 적용시키려면 어떻게써야하나요??

      • 한번 gist.github.com 에 코드 올려보시겠어요?
        post 요청을 했을 때 결과값이 어떻게 나타나나요?
        { result: 1 }은 됐는데 저장이 안된건가요 아니면 [] 를 리턴하는건가요?

        [] 를 리턴하는경우엔 라우팅이 잘못된것 같은데,
        테스팅 할 때 잘못한거일수도있구요.

        http://url/api/books 로 요청한것 맞나요?

        • 헉 이렇게 빨리 답변이 달리다니ㅠㅠ감사합니다

          아 제가 post 요청하는 법을 몰라서 그랬던거네요..post요청은 브라우저만으로는 못하는건가요??
          워낙 초보라서 검색을 해보고왔는데도 잘 모르겠습니다…
          insomnia로는 데이터가 잘 들어갔는데 post맨으로는 제가 넣는 데이터는 안들어가고 디폴트값만 생성되네요 body에서 raw체크해 insomnia처럼 입력해도 안들어가고 form-data에서 키,밸류 입력해도 안들어가서
          Headers랑 params탭으로도 입력해봤지만 디폴트값만 생성되고 데이터는 안들어가네요 제가 또 몰라서 이런 질문을 드리는 걸까요?ㅠㅠㅠ

          • raw를 체크하고 우측에 JSON으로 하였는지 체크해보세요.

            참고이미지: https://velopert.com/wp-content/uploads/2016/02/edd.png

            POST 요청의 경우엔 브라우저의 form 에서 submit 을 해야 사용이 가능합니다. 브라우저에 주소창을 쓰는 것 만으로는 안되구요.

            REST API 는 보통 form 으로 submit 하는 방식이 아니라 웹클라이언트 – jQuery 의 ajax, Angular의 http, 혹은 기타 웹클라이언트 라이브러리 – superagent axios fetch등을 사용합니다.

          • 감사합니다

            잘 됩니다 감사해요ㅠㅠ!!!!!!!

  • 궁금이

    안녕하세요 강좌 정말 잘보고 있습니다. 간단한 질문 사항이 있어 이렇게 답변 남겨 드립니다.
    현재는 Book이라는 특정 model을 연결시키며

    var router = require(‘./routes’)(app, Book);

    위와 같이 라우터에 Book객체를 던지는데 다른 모델도 사용하여 동적으로 사용하라면 어떻게 하는것이 가장 좋은 방법인가요?

    • 현재는 var Book = require(‘./models/book’); 를 app.js에서하고 라우터로 던져줬었는데

      이렇게 할 필요없이 라우터 파일에서
      var Book = require(‘./models/book’); 를 하고 사용해도 됩니다.

      라우터도 app을 파라미터로 던져줄 필요 없이
      라우터 클래스를 사용해서 모듈화 할수 있구요

      http://expressjs.com/ko/guide/routing.html

      제가 강좌를 쓸 때 상당히 오래된 자료를 보고 작성했던것 같아요. 지금 보니까 수정할곳이 좀 많네요.. ㅋㅋ 조만간 강좌들을 수정해야겠어요..

  • Pyunghwa Seo

    models/bear.js 이 부분은 오타인 거 같네요 ㅎ
    bear.js 가 아니라 book.js?

  • 장현정

    안녕하세요 좋은 글 잘 읽었습니다.

    GET, POST는 웹 브라우저, form을 포함한 html 파일 등으로 URI 요청이 가능하여 브라우저 만으로 테스트가 잘 됩니다.

    자료의 목록을 보고,
    이름을 클릭하여 Update form을 호출하여 데이터를 보여주고
    수정한 후 app.put으로 업데이트를 하려면 form에 method=put으로 설정하면 되는지요

    그럼 삭제 버튼을 눌렀을때
    app.delete router를 호출하려면 html에 어떻게 코딩을 해야 하는지요

    이전에든
    글제목 이렇게 했습니다만.

    REST API에서는 어떻게 코딩을 해야하는지 감이 잡히지 않습니다

    • HTML Form은 GET과 POST만 지원합니다.
      이 외의 메소드 들은 ajax요청을 통하여 호출해야합니다.

      • 장현정

        아 생가했던 대로 그렇군요.
        실시간 답변 감사합니다.
        행복한 주말 되십시요 ^

      • 장현정

        아 그렇군요. 감사합니다 ^^
        새해 복 많이 받으십시요.

  • GT M

    index.js 에서 Book에서 쓰는 메서드를 MongoDB강좌 때 나왔던 메서드들 과 동일하게 쓸수있는건가요 ?

  • Jong Min Moon

    안녕하세요! 글 잘보고 있습니다 어느덧 여기까지 왔네요 다름이 아니라 질문이 있어서 이렇게 글을 남깁니다 혹시 만약에 코맨트같은걸 달아야한다면 어떻게 하면 좋을까요? upsert?인가 찾아봤는데 계속데이터를 업데이트만 하더라구요~

  • 지나는버섯

    특정 collection을 사용하려면 schema 생성시에 collection 명을 지정해야 하는데, 이 collection을 동적으로 매핑하는 것도 가능한가요? 날짜별로 구분된 같은 schema를 가지는 collection이 여러개 있어서, 날짜를 파라미터로 받아 colleciton 내에서 find를 하려고 하는데, 동적 매핑이 가능할지를 검색해 봐도 영 나오질 않네요ㅠㅠ

  • Seonghun Kang

    안녕하세요. 강좌 따라하다가 생긴 일을 저같은 분이 계실까 하여 공유드립니다.
    폴더명을 mongoose로 했었는데 이 때, npm install –save express mongoose body-parser 커맨드에 입력했을 때, 오류가 났었습니다. 그 원인은 바로 폴더명이 패키지명과 같아서 인 것 같습니다.
    폴더명을 ex_mongoose로 변경 후 재입력했을 때는 오류가 해결되었습니다.

  • 쳐부수자

    var bookSchema = new Schema({
    title: String,
    author: String,
    published_date: { type: Date, default: Date.now }
    },{collection: ‘book’});
    왠지 모르겠지만 다른 collection은 잘 찾아오는데 어떤 collection은 찾지 못하고 빈 배열만 반환해서 방법을 찾다가 위와 같이 schema에 collection명을 명시해주니 됐습니다. 왜 어떤건 되고 어떤건 안되는지 이유는 모르겠네요 ㅠㅠ

  • 박지환

    실행은 Docker에 우분투깔아서 했습니다.
    몽구스 실행시에 node_modules/mongoose/lib/index.js 여기에 const때문에
    node –harmony app.js 실행시에 const 에러는 잡았는데…
    return conn.openUri(arguments[0], arguments[1], arguments[2]).then(() => this)
    ^
    SyntaxError: Unexpected token )
    이렇게 에러가 잡히네요…. 구문손댄건 없구요… 저 구문을 람다식으로 사용안하면 다음 람다식으로 에러걸리네요

    어떻게 해결할수있을까요…

  • 김진렬

    감사합니다.