함께 성장하는 프로독학러
3. mongoose - Node.js 에서 MongoDB 를 사용할 수 있도록 하는 모듈 본문
안녕하세요, 프로독학러 입니다.
이번 포스팅에서는 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 를 사용할 수 있습니다.)
1. mongoose 설치
mogoose 는 npm 패키지 매니저를 통해서 설치할 수 있습니다.
(프로젝트 경로에서 아래 명령어를 통해 설치합니다)
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 가 제대로 작동하는지 테스트해 보겠습니다.
위 명령어를 통해 app.js 파일을 실행시켜보겠습니다.
서버가 정상적으로 실행되면 브라우저에 아래의 주소를 입력해 들어가 보도록 하겠습니다.
라우터에서 입력한대로 '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: Date, default: Date.now } }); export default mongoose.model('book', bookSchema); | cs |
코드의 네 번째 줄에서 new Schema 를 통해 새로운 스키마를 만들었습니다.
schema 에서 사용되는 SchemaType 은 총 여덟 종류가 있습니다.
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- Objectid
- 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 실행)
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 === 0) return 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)
감사합니다.
**참고 자료 (항상 감사드립니다)
*이 포스팅이 도움이 되셨다면 다녀가셨다는 표시로 공감 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)
'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 |