데이터베이스로 웹 서비스에서 사용되는 데이터를 저장하고 효율적으로 조회 및 수정할 수 있다. 기존에는 MySQL, OracleDB 같은 관계형 데이터베이스를 자주 사용했다.

스키마가 고정적
데이터 형식이 기존의 데이터와 다르다면 기존 데이터를 모두 수정해야 새 데이터를 등록할 수 있다. 데이터 양이 많을 때 번거롭다.
스키마는 DB의 구조와 제약 조건에 관한 전반적인 명세를 정의한 메타데이터의 집합이다. 자세히 말하면, 개체의 특성을 나타내는 속성과, 속성들의 집합으로 이루어진 개체, 개체 사이에 존재하는 관계에 대한 정의와 이들이 유지해야 할 제약 조건을 기술한 것이다.
확장성
저장하고 처리해야 할 데이터 양이 늘어나면 여러 컴퓨터에 분산시키는 것이 아니라 해당 데이터베이스 서버의 성능을 업그레이드하는 방식으로 확장해줘야 한다.
MongoDB는 위의 한계를 극복한 문서 지향적
NoSQL DB이다. 현존하는NoSQL DB중에서 1위를 유지하고 있다.
❗NoSQL은 Not Only SQL 의 약자이다.
- 데이터들은 유동적인 스키마를 지닐 수 있고, 새로 등록할 데이터의 형식이 바뀐다해도 기존 데이터를 수정할 필요가 없다.
- 데이터 양이 늘어난다면 여러 컴퓨터에 분산하여 처리할 수 있도록 확장하기 쉽게 설계되었다.
문서는 관계형 데이터 베이스의 record와 비슷한 개념이다. 문서의 데이터 구조는 1개 이상의 key-value 쌍으로 이뤄져있다.
📄 문서
{
"_id": ObjectId("5099803df3f4948bd2f98391"),
"username": "nabang",
"name": { first: "HyeSoo", last: "Na" }
}
📂 컬렉션
여러 문서가 들어있는 곳을 컬렉션이라고 한다. 기존 RDBMS는 테이블 개념을 사용하기에 각 테이블마다 같은 스키마를 가지고 있어야 한다. MongoDB는 다른 스키마를 가지고 있는 문서들이 한 컬렉션에 존재할 수 있다.
{
"_id": ObjectId("594948a081ad6e0ea526f3f5"),
"username": "nabang"
},
{
"_id": ObjectId("59494fca81ad6e0ea526f3f6"),
"username": "na",
"phone": "010-3333-6666"
}

기존 RDBMS에서 블로그용 데이터 스키마를 디자인한다면 각 포스트, 댓글마다 테이블을 만들어 필요에 따라 JOIN해서 사용하는 것이 일반적이다.

하지만 NoSQL DB 에서는 한 문서에 최대한 많은 데이터를 넣는다. 즉, 포스트 내부에 댓글 배열을 넣는다. 이를 subdocument라고 하며, subdocument도 일반 문서를 다루는 것처럼 쿼리할 수 있다.
{
_id: ID,
title: string,
body: string,
username: string,
createDate: Date,
comments: [
{
_id: ID,
text: string,
createDate: Date
}
]
}
한 문서에는 최대 16MB 데이터를 넣을 수 있다. 만약 이 용량을 초과할 가능성이 있다면 컬렉션을 분리하는 것이 좋다.
MongoDB 공식 홈페이지에서 설치파일을 다운로드한다.
MongoDB 기본 설치 경로 : C:\Program Files\MongoDB\Server\버전\bin\
터미널을 열어 해당 디렉토리로 이동하고 mongod 명령어를 입력해 서버를 실행한다.

// mongoose, dotenv 설치
$ yarn add mongoose dotenv
Mongoose는 MongoDB 기반 ODM (Object Data Modelling) 라이브러리이다. 이 라이브러리는 데이터베이스 문서들을 JavaScript 객체처럼 사용할 수 있게 해준다.
dotenv는 환경변수들을 파일에 넣고 사용할 수 있게 해주는 개발도구이다. mongoose를 연결할 때 서버에 대한 계정과 비밀번호를 입력하게 되는데, 이런 민감한 정보는 코드에 직접 작성하지 않고 환경변수로 설정하는 것이 좋다.

🏷️ .env
환경변수에는 서버에서 사용할 포트와 MongoDB 주소를 넣어준다. 프로젝트의 루트 경로에 .env 파일을 생성하여 다음과 같이 입력한다.
PORT=4000
MONGO_URI=mongodb://127.0.0.1:27017/blog
여기서 blog는 우리가 사용할 데이터베이스 이름이다. 지정한 데이터베이스가 서버에 없다면 자동으로 만들어주므로 사전에 따로 생성할 필요는 없다.
🏷️ src/index.js
// 1. dotenv를 불러와서 config( ) 함수를 호출
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
// 2. 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT } = process.env;
const api = require('./api')
const app = new Koa();
const router = new Router()
// 라우터 설정
router.use('/api', api.routes())
// 라우트 적용 전에 bodyparser 적용
app.use(bodyParser())
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods())
// 3,4. PORT가 지정되어 있지 않다면 4000을 사용
const port = PORT || 4000;
app.listen(port, () => {
console.log('Listening to port', port);
});
이제 mongoose를 이용하여 서버와 데이터베이스를 연결해보자. 연결할 때는 mongoose의 connect 함수를 사용한다.
// dotenv를 불러와서 config( ) 함수를 호출
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const mongoose = require('mongoose');
// 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URI } = process.env;
// connect
mongoose
.connect(MONGO_URI, { useNewUrlParser: true, useFindAndModify: false })
.then(() => {
console.log('Connected to MongoDB');
})
.catch(e => {
console.error(e);
});
const api = require('./api')
const app = new Koa();
const router = new Router()
// 라우터 설정
router.use('/api', api.routes())
// 라우트 적용 전에 bodyparser 적용
app.use(bodyParser())
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods())
// PORT가 지정되어 있지 않다면 4000을 사용
const port = PORT || 4000;
app.listen(port, () => {
console.log('Listening to port', port);
});
코드를 저장한 뒤 터미널에 다음과 같은 문구가 출력되면 데이터베이스에 성공적으로 연결된 것이다.
[nodemon] starting `node src/index.js`
Listening to port 4000
Connected to MongoDB
이제 mongoose를 사용하기 위한 준비를 마쳤다.

Node.js v12부터 ES Module import/export 문법이 정식으로 지원된다. Node.js v12를 사용할 경우, package.json에서 "type": "module" 을 추가하면 ES Module을 바로 사용할 수 있다.

api/posts/posts.ctrl.js 파일을 열어서 exports 코드를 export const로 모두 변환한다.
let postId = 1; // id의 초기값
// posts 배열 초기 데이터
const posts = [
{
id: 1,
title: "제목",
body: "내용",
},
];
// 1. 포스트 작성 : POST /api/posts
// {title, body}
export const write = (ctx) => {
const { title, body } = ctx.request.body; // REST API의 Request body는 ctx.request.body에서 조회
postId += 1;
const post = { id: postId, title, body };
posts.push(post);
ctx.body = post;
};
// 2. 포스트 목록 조회 : GET /api/posts
export const list = (ctx) => {
ctx.body = posts;
};
// 3. 특정 포스트 조회 : GET /api/posts/:id
export const read = (ctx) => {
const { id } = ctx.params;
// 파라미터로 받아온 값이 문자열 형식이므로 id 값을 문자열로 변경함
const post = posts.find((p) => p.id.toString() === id);
// 포스트가 없으면 오류 반환
if (!post) {
ctx.status = 404;
ctx.body = {
message: "포스트가 존재하지 않습니다.",
};
return;
}
ctx.body = post;
};
// 4. 특정 포스트 제거 : DELETE /api/posts/:id
export const remove = ctx => {
const {id} = ctx.params
// 해당 id를 가잔 post가 몇 번째인지 확인
const index = posts.findIndex(p => p.id.toString() === id)
if ( index === -1 ){
ctx.status = 404
ctx.body = {
message: '포스트가 존재하지 않습니다.'
}
return
}
posts.splice(index,1)
ctx.status = 204 // No Content
}
// 5. 포스트 수정 : PUT /api/posts/:id
// {title, body}
// PUT 메소드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용!
export const replace = ctx => {
const {id} = ctx.params
// 해당 id를 가잔 post가 몇 번째인지 확인
const index = posts.findIndex(p => p.id.toString() === id)
if ( index === -1 ){
ctx.status = 404
ctx.body = {
message: '포스트가 존재하지 않습니다.'
}
return
}
// 전체 객체를 덮어 씌운다. id를 제외한 기존 정보를 날리고 객체를 새로 만든다.
posts[index] = {
id,
... ctx.request.body
}
ctx.body = posts[index]
}
// 6. 포스트 수정 (특정 필드) : PATCH /api/posts/:id
// {title, body}
export const update = ctx => {
const {id} = ctx.params
// 해당 id를 가잔 post가 몇 번째인지 확인
const index = posts.findIndex(p => p.id.toString() === id)
if ( index === -1 ){
ctx.status = 404
ctx.body = {
message: '포스트가 존재하지 않습니다.'
}
return
}
// 기존 값에 정보를 덮어씌움
posts[index] = {
...posts[index],
... ctx.request.body
}
ctx.body = posts[index]
}
src/api/posts/index.js 파일을 수정한다.
import Router from 'koa-router';
import * as postsCtrl from './post.ctrl.js';
const posts = new Router()
posts.get('/',postsCtrl.list)
posts.post('/',postsCtrl.write)
posts.get('/:id',postsCtrl.read)
posts.delete('/:id',postsCtrl.remove)
posts.put('/:id',postsCtrl.replace)
posts.patch('/:id',postsCtrl.update)
export default posts

src/api/index.js 파일을 수정한다.
import Router from 'koa-router';
import posts from './posts/index.js';
const api = new Router()
api.use('/posts', posts.routes())
// 라우터를 내보냅니다.
export default api;
src/index.js 파일을 수정한다.
// dotenv를 불러와서 config( ) 함수를 호출
import dotenv from 'dotenv'
dotenv.config()
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import mongoose from 'mongoose';
import api from './api/index.js';
// 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URI } = process.env;
( ... )
이제 Postman으로 http://localhost:4000/api/posts 에 요청을 보내 우리가 만든 서버가 오류 발생으로 종료되지 않고 잘 작동하는지 확인해보자.

mongoose에는 스키마 schema & 모델 model 개념이 있다.
우리는 블로그 포스트에 대한 스키마를 생성할 것이다.
포스트 하나에 이렇게 총 4가지 정보가 필요하다. 각 정보에 대한 필드 이름과 데이터 타입을 설정한다.

스키마와 모델에 관련된 코드는 src/models 디렉토리에 작성할 것이다. 이렇게 디렉토리를 따로 만들어 관리하면 나중에 유지 보수를 좀 더 편하게 할 수 있다. models 디렉토리를 만들고, 그 안에 post.js 파일을 만들어 다음 코드를 작성한다.