함께 성장하는 프로독학러

3. mongoose - Node.js 에서 MongoDB 를 사용할 수 있도록 하는 모듈 본문

Programming/Node.js

3. mongoose - Node.js 에서 MongoDB 를 사용할 수 있도록 하는 모듈

프로독학러 2018. 6. 16. 16:09

안녕하세요, 프로독학러 입니다.


이번 포스팅에서는 mongoose 에 대해서 알아보도록 하겠습니다.


*이 포스팅은 velopert 님의 Node.js 강좌 11편을 복습한 내용입니다. velopert 님의 원본 포스팅을 보고싶으신 분들은 아래의 링크를 참고해 주세요.

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


mongoose 는 Node.js 환경에서 MongoDB 에 접근하여 데이터베이스를 조회 및 수정, 삭제 등을 가능하게 해 주는 모듈입니다.


mongoose 는 Node.js 와 MongoDB 를 연결해 주는 브릿지 같은 역할을 수행하므로, 먼저 MongoDB 가 설치되어 있어야 사용 가능합니다.

*MongoDB 를 설치하지 않으신 분은 아래의 링크를 통해 MongoDB 를 먼저 설치해주세요. (MongoDB 에 익숙치 않으신 분들을 아래 링크의 카테고리에 해당하는 포스팅들을 모두 읽어보면 좀 더 쉽게 mongoose 를 사용할 수 있습니다.)

<MongoDB 소개 및 설치, 실행>


1. mongoose 설치


mogoose 는 npm 패키지 매니저를 통해서 설치할 수 있습니다.

(프로젝트 경로에서 아래 명령어를 통해 설치합니다)


npm install --save mongoose body-parser


mongoose : Node.js 환경에서 MongoDB 를 연동해주는 라이브러리

body-parser : 데이터 처리 미들웨어 (req.body 를 통하여 요청에 접근할 수 있도록 해줍니다)


2. 서버 설정


2-1) 프로젝트 디렉토리 구조


mongoose 를 활용한 예제를 위해 필요한 프로젝트의 디렉토리는 아래와 같습니다.


  • models : Document 의 구조가 어떻게 생겼는지 알려주는 역할을 하는 파일들이 들어갈 디렉토리입니다.
  • routes : 라우터가 위치할 디렉토리입니다.


우선 프로젝트 폴더 하위에 위의 두 디렉토리를 생성해 줍시다.

폴더안의 파일들은 예제를 진행하면서 만들도록 하겠습니다.


2-2) 각 라우터들의 경로와 메소드, 역할


우리는 예제로 MongoDB 에서 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 데이터 수정 (_id 값에 해당하는) 

/api/books/:book_id 

DELETE 

book 데이터 수정 (_id 값에 해당하는) 


2-3) 서버의 메인 파일 - app.js


먼저 프로젝트의 루트 경로에 app.js 파일을 생성합니다.

app.js 의 내용은 아래와 같습니다.


(./app.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app.js - 서버 메인 파일
// [LOAD PACKAGES]
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose'
const app = express();
 
// [CONFIGURE APP TO USE bodyParser]
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
 
// [CONFIGURE SERVER PORT]
var port = 8080;
 
// [CONFIGURE ROUTER]
import api from './routes'
app.use('/api', api);
 
// [RUN SERVER]
app.listen(port, () => {
  console.log("Express server has started on port " + port)
});
cs



코드의 3~5 번째 줄에서 위에서 예제에 필요한 모듈들을 import 했습니다.

6 번째 줄에서 app 상수에 express 를 실행해 할당했습니다.


9~10 번째 줄에서 body-parser 를 사용하도록 설정했습니다.

(request 객체의 body 에 대한 확장을 할 수 있도록 설정하고, req 객체의 body 값을 json 형식으로 인코딩하도록 설정했습니다)


13 번째 줄에서 port 를 8080 으로 설정하고 해당 포트를 20 번째 줄에서 리스닝 했습니다.


16번째 줄에서 routes 폴더를 api 로 임포트 했는데, 이를 17번째 줄에서 '/api' 경로에서 사용하도록 하였습니다.

만약 routes 폴더 안의 index.js 파일에 router 를 '/books' 경로로 설정하면 이는 '/books' 이 아니라 '/api/books' 을 의미합니다.

(app.use 로 '/api' 경로 사용, routes 에서 '/books' 경로 사용 => '/api/books' 경로)


그럼 라우터에 해당하는 파일을 작성해 보도록 합시다.

routes 디렉토리에 index.js 파일을 생성합니다. (어떤 디렉토리에 index.js 파일은 디렉토리 자체를 의미합니다. import 할 때 './routes/index.js' 로 하지 않고 './routes' 와 같이 폴더만을 import 해도 자동으로 index.js 파일에 접근합니다.)


(./routes/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import express from 'express';
const router = express.Router();
 
// GET ALL BOOKS
router.get('/books', (req, res) => {
  res.send('GET: /api/books!');
});
 
// GET SINGLE BOOK
router.get('/books/:book_id', (req, res) => {
  res.send('GET: /api/books/:book_id!');
});
 
// GET BOOK BY AUTHOR
router.get('/books/author/:author', (req, res) => {
  res.send('GET: /api/books/author/:author!');
});
 
// CREATE BOOK
router.post('/books', (req, res) => {
  res.end();
});
 
// UPDATE THE BOOK
router.put('/books/:book_id', (req, res) => {
  res.end();
});
 
// DELETE BOOK
router.delete('/books/:book_id', (req, res) => {
  res.end();
});
 
export default router;
 
cs


코드의 두 번째 줄에서 express.Router() 를 router 상수에 할당했습니다.

그리고 각각 API 에 맞게 라우터를 설정했습니다.


API 가 제대로 작동하는지 테스트해 보겠습니다.


babel-node app.js 


위 명령어를 통해 app.js 파일을 실행시켜보겠습니다.


서버가 정상적으로 실행되면 브라우저에 아래의 주소를 입력해 들어가 보도록 하겠습니다.


localhost:8080/api/books



라우터에서 입력한대로 'GET: /api/books!' 이 잘 뜨는 것을 알 수 있습니다.


3. MongoDB 서버 연결


Mongoose 를 이용하여 MongoDB 서버에 연결하는 방법은 다음과 같습니다. 


(./app.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app.js - 서버 메인 파일
// [LOAD PACKAGES]
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose'
const app = express();
 
// CONNECT TO MONGODB SERVER
const 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');
 
// [CONFIGURE APP TO USE bodyParser]
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
 
// [CONFIGURE SERVER PORT]
var port = 8080;
 
// [CONFIGURE ROUTER]
import api from './routes'
app.use('/api', api);
 
// [RUN SERVER]
app.listen(port, () => {
  console.log("Express server has started on port " + port)
});
 
cs


코드의 9 번째 줄에서 mongoose 의 connection 메소드를 상수 db 에 할당 했습니다.

10 번째 줄은 MongoDB 연결에 오류가 생겼을 때 알려주는 코드입니다.

11번째 줄은 연결에 성공했을 때 콘솔창에 "Connected to mongod server" 를 뜨도록 하는 코드입니다.

15번째 줄은 MongoDB 에서 어떤 데이터베이스를 사용할지 지정하는 코드입니다.

여기서는 mongodb_tutorial 데이터베이스를 사용합니다.


MongoDB 서버를 먼저 실행시킨 뒤, app.js 를 babel-node 를 통해서 실행시켜 봅시다.

(MongoDB 서버는 MongoDB 설치 경로에서 mongod 를 통해 실행시킵니다)



MongoDB 서버에 연결되면 위와 같이 "Connected to mongod server" 가 뜹니다.


4. Schema & Model


4-1) schema


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


각 document 의 필드의 값에 어떤 데이터 타입이 들어올지 알려주는 역할을 하는것입니다.


models 디렉토리에 book.js 파일을 만들고 아래와 같이 작성해 주세요.


(./models/books.js)

1
2
3
4
5
6
7
8
9
10
11
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
 
const bookSchema = new Schema({
    title: String,
    author: String,
    published_date: { type: DatedefaultDate.now  }
});
 
export default mongoose.model('book', bookSchema);
 
cs


코드의 네 번째 줄에서 new Schema 를 통해 새로운 스키마를 만들었습니다.


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


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


이를 자세히 사용하는 방법은 mongoose 의 메뉴얼을 참고해 주세요.


코드의 10 번째 줄에서 export 할 때 mongoose.model 을 익스포트 했습니다. 


4-2) model


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


1
const Book = mongoose.model('book', bookSchema);
cs


첫 번째 인자는 해당 도큐먼트가 사용할 collection 의 단수적 표현입니다. 따라서 실제로 사용하는 collection 은 'books' 입니다. mongoose.model 의 첫 번째 인자로 들어오는 값을 자동으로 복수화 시켜서 collection 이름으로 사용합니다.

만약 자동으로 복수화 시켜 collection 명을 사용하는 것이 아니라 collection 명을 임의대로 정하고 싶다면 스키마를 만들 때 따로 설정해주면 됩니다. (new Schema 의 두 번째 인자를 준다)


1
const dataSchema = new Schema({ 도큐먼트의 구조 정의 }, { collection: 'COLLECTION_NAME' });
cs


model 을 만들고나면, 모델을 사용하여 아래 코드와 같이 DB 에 저장하거나 조회할 수 있습니다.


1
2
3
4
5
6
7
8
9
var book = new Book({
    name"NodeJS Tutorial",
    author: "velopert"
});
 
book.save(function(err, book){
    if(err) return console.error(err);
    console.dir(book);
});
cs


우리는 models/book.js 에서 schema 를 통해 데이터 구조를 결정하고, 해당 스키마를 사용할 collection 을 지정하여 model 을 익스포트 했습니다.

즉, model 을 모듈화 한 것입니다. 따라서 우리는 book.js 를 임포트하면 new Book() 을 통해 데이터베이스에서 데이터를 읽거나 생성, 수정 등을 할 수 있습니다.

* console.log 는 HTML 구조로 출력, console.dir 은 JSON 구조로 출력합니다.


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


CRUD 는 데이터를 생성, 조회, 수정, 삭제를 의미합니다.

위에서 만든 모델을 통해 books 콜렉션에 새로운 도큐먼트를 생성하거나 조회, 수정, 삭제하는 방법에 대해서 알아보겠습니다.

CRUD 는 Book 모델을 통해 이루어지므로, 라우터에서 Book 모델을 import 해 줍니다.


(./routes/index.js)

1
2
3
4
5
6
7
...
 
import Book from '../models/books';
 
... ROUTES ...
 
export default router;
cs


세 번째 줄과 같이 models/book 을 Book 으로 import 해 줍니다.


5-1) Create (POST: /api/books)


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


(./routes/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// CREATE BOOK
router.post('/books', (req, res) => {
  var book = new Book();
  book.title = req.body.title;
  book.author = req.body.author;
  book.published_date = new Date(req.body.published_date);
 
  book.save( (err) => {
      if(err){
          console.error(err);
          res.json({result: 0});
          return;
      }
 
      res.json({result: 1});
 
  });
});
cs


코드의 세 번째 줄에서 새로운 모델을 만들고 변수 book 에 할당했습니다.

그리고 4~6 번째 줄에서 사용자로부터 입력받은 값(body-parser 를 통해 req.body 를 통해 접근가능)을 title, author, published_date 필드에 값으로 전달했습니다.


코드의 8 번째 줄에서 .save() 메소드를 통해 데이터를 데이터베이스에 저장합니다. 

오류가 발생하면 { result: 0 } 을 리스폰스하고, 성공하면 { result: 1 } 을 리스폰스합니다.


Postman 을 통해 제대로 작동하는지 확인해 보도록 하겠습니다. (MongoDB 서버연결, app.js 실행)

<Postman 사용법>



localhost:8080/api/books 의 body 로 위와 같이 json 형태의 데이터를 보내자 response 로 { result: 1 } 이 왔습니다.


MongoDB shell 을 통해 데이터가 제대로 추가되었는지 확인해 봅시다.

(mongoDB 설치 경로에서 mongo )



books 콜렉션에 데이터가 제대로 저장된 것을 알 수 있습니다.


(아래의 예제를 위해 여러개의 데이터를 데이터베이스에 추가했습니다.)


5-2) Retrieve (GET: /api/books)


데이터를 조회할 때는 .find() 메소드를 사용합니다. 

메소드의 첫 번째 인자는 Query 파라메터 입니다. Query 파라메터가 전달되면 해당 쿼리를 기준으로 조건에 부합되는 document 만을 조회합니다.

메소드의 두 번째 인자는 projection 객체 입니다. 검색 결과중 어떤 필드를 표시할지 결정합니다.

메소드의 세 번째 인자는 콜백함수로, 콜백함수의 첫 번째 인자는 err 이고, 두 번째 인자는 검색 결과(조회된 데이터)입니다.


5-2-1) 모든 데이터 조회


(./routes/index.js)

1
2
3
4
5
6
7
// GET ALL BOOKS
router.get('/books', (req, res) => {
  Book.find( (err, books) => {
      if(err) return res.status(500).send({error: 'database failure'});
      res.json(books);
  })
});
cs


위의 코드는 find 메소드에 Query 파라메터가 전달되지 않았으므로 books 콜렉션의 모든 다큐먼트를 조회합니다.


postman 을 통해 확인해 보면 다음과 같이 콜렉션 안의 모든 데이터가 조회되는것을 알 수 있습니다.



5-2-2) Query 조건에 만족하는 데이터 조회


이번에는 모든 데이터를 조회하는 것이 아니라 author 값이 매칭되는 데이터를 조회하는 API 를 작성해 보도록 하겠습니다.


(./routes/index.js)

1
2
3
4
5
6
7
8
// GET BOOK BY AUTHOR
router.get('/books/author/:author', (req, res) => {
  Book.find({author: req.params.author}, {_id: 0, title: 1, published_date: 1}, (err, books) => {
        if(err) return res.status(500).json({error: err});
        if(books.length === 0return res.status(404).json({error: 'book not found'});
        res.json(books);
    })
});
cs


추가적으로 find() 메소드에 projection 객체를 전달해 title 과 published_date 필드만 표시하도록 했습니다.


postman 에서 'localhost:8080/api/books/author/test' 로 test 가 작성한 메모를 조회해 보도록 하겠습니다.



test 가 작성한 데이터가 잘 조회되는 것을 알 수 있습니다.


5-2-3) 조건에 만족하는 하나의 데이터 조회


이번에는 _id 값으로 하나의 다큐먼트만을 조회하는 API 를 작성해 보겠습니다.


(./routes/index.js)

1
2
3
4
5
6
7
8
// GET SINGLE BOOK
router.get('/books/:book_id', (req, res) => {
  Book.findOne({_id: req.params.book_id}, (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);
    })
});
cs


findOne() 메소드는 Query 조건에 부합하는 하나의 다큐먼트만을 조회합니다.


postman 을 통해 확인해 보겠습니다.



url 에 들어온 id 값으로 다큐먼트를 잘 조회하는 것을 알 수 있습니다.


5-3) Update (PUT: /api/books/:book_id)


document 를 수정하는 방법은 두 가지가 있습니다. 

첫 번째는 데이터를 먼저 조회한 뒤, save() 메소드를 통하여 저장하는 것이고, 

두 번째는 update() 메소드를 이용하는 것입니다. 두 방법 모두 데이터를 수정하는 측면에서 똑같지만, 두 번째 방법은 업데이트 하는 과정에서 document 를 조회하지 않습니다.


(./routes/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UPDATE THE BOOK
router.put('/books/:book_id', (req, res) => {
  Book.findById(req.params.book_id, (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( (err) => {
            if(err) res.status(500).json({error: 'failed to update'});
            res.json({message: 'book updated'});
        });
    });
});
cs


데이터를 조회하는 메소드는 findById 도 있습니다. 

findById 메소드는 query 객체를 인자로 전달하지 않고 id 값을 바로 (String) 첫 번째 인자로 전달합니다.


에러가 발생하면 어떤 에러인지 리스폰스하는 코드는 4~5 번째 줄에 걸쳐 작성하였습니다.

7~9 번째 줄에서는 입력받은 데이터(req.body) 가 존재하면 해당 값으로 수정하는 코드입니다.

그리고 수정이 완료되면 11 번째 줄에서 save 메소드를 통하여 DB 에 변경된 값을 저장합니다.


위의 코드를 update 메소드를 활용하면 다음과 같이 작성할 수 있습니다.


(./routes/index.js)

1
2
3
4
5
6
7
8
9
/ UPDATE THE BOOK (ALTERNATIVE)
app.put('/api/books/:book_id'function(req, res){
    Book.update({ _id: req.params.book_id }, { $set: req.body }, (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' } );
    })
});
cs


위의 코드에서 output.n 은 mongod 에서 출력하는 결과물로, select 된 도큐먼트의 갯수입니다.


postman 을 통해 제대로 작동하는지 확인해 보도록 하겠습니다.



_id 가 5b24a55b90af4f1d08948a90 인 도큐먼트의 내용을 위와 같이 수정했습니다.


해당 도큐먼트를 GET: /api/books/:book_id 로 조회해보면 수정된 내용이 잘 반영된 것을 알 수 있습니다.



5-4) Delete (DELETE: /api/books/:book_id)


도큐먼트를 제거할 떄는 remove() 메소드를 사용합니다.


메소드의 첫 번째 인자는 Query 로 해당 조건에 만족하는 도큐먼트를 삭제합니다.


(./routes/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
// DELETE BOOK
router.delete('/books/:book_id', (req, res) => {
  Book.remove({ _id: req.params.book_id }, (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();
  })
});
cs


HTTP 메소드 중 DELETE 는 idempotent 입니다. 

(어떤 과정을 몇 번이고 반복 수행하더라도 결과가 동일함. 즉, 삭제한 데이터를 삭제해도, 존재하지 않는 데이터를 삭제해도 결과가 달라지지 않음)

따라서 성공하거나 실패하더라도 결과 값이 같습니다. 

HTTP status 204 는 No Content 로 요청한 작업을 수행하였고 데이터를 반환할 필요가 없다는 것을 의미합니다.

* 6~9번째 줄을 실제 존재하는 데이터를 삭제했는지 확인하는 코드이나 그럴 필요가 없으므로 주석처리


postman 을 통해 데이터 삭제가 잘 되는지 확인해 보겠습니다.




제대로 삭제 된것을 확인 할 수 있습니다.


여기까지...


mongoose 를 통해 Node.js 환경에서 MongoDB 를 제어하는 방법에 대해서 알아보았습니다.

간단히 요약하자면, schema 를 통해 데이터의 모양을 갖추고, 만든 schema 를 사용할 collection 을 지정하여 model 을 익스포트합니다.

그리고 해당 model 을 API 에서 임포트하여 데이터를 조회, 수정, 삭제, 생성등의 작업을 합니다. 

(생성만 new 모델을 통해 새로운 모델을 만들어 저장-save)


감사합니다.


**참고 자료 (항상 감사드립니다)

https://velopert.com/594/


*이 포스팅이 도움이 되셨다면 다녀가셨다는 표시로 공감 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

'Programming > Node.js' 카테고리의 다른 글

4. pbkdf2-password, 비밀번호 보안 모듈  (0) 2018.06.18
2. Express  (1) 2018.06.15
1. 동기와 비동기, 콜백함수  (6) 2018.05.23
0. Nodejs 소개 및 설치  (0) 2018.05.22
Comments