MongoDB는 문서 지향적 NoSQL DB입니다.
새로 등록해야 할 데이터의 형식이 바뀐다고 하더라도 기존 데이터까지 수정할 필요가 없습니다.
그리고 서버의 데이터 양이 늘어나도 한 컴퓨터에서만 처리하는 것이 아니라 여러 컴퓨터로 분산하여 처리할 수 있도록 확장하기 쉽게 설계되었습니다.
컬렉션: 여러 문서가 들어 있는 곳
MongoDB는 다른 스키마를 가지고 있는 문서들이 한 컬렉션에 공존할 수 있습니다.
MongoDB 구조
서버 하나에 여러 개의 DB를 가지고 있을 수 있으며, 각 DB에는 여러개의 컬렉션이 있고, 컬렉션 내부에는 문서들이 들어있습니다.
몽고디비는 주소를 사용해 연결합니다. ( 호스트:포트번호/데이터베이스 이름 )
MONGO_URL = mongodb://localhost:27017/auth
mongoose는 MongoDB ODM 중 가장 많이 쓰이고 있습니다.
ODM(Object Document Mapping) Object: 자바스크립트 객체, Document: 몽고DB의 문서
즉, 문서를 DB에서 조회할 때 자바스크립트 객체로 바꿔 주는 역할...
models/index.js 파일에 DB와 연결하는 코드를 작성하고
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
const { MONGO_URL } = process.env;
// DB 연결 함수
export const connect = () =>{
// 개발 환경이 아닐 때 몽구스가 생성하는 쿼리 내용을 콘솔에 출력
if(process.env.NODE_ENV !== 'production'){
mongoose.set('debug', true);
}
mongoose
.connect(MONGO_URL, { useNewUrlParser: true, useFindAndModify: false })
.then(()=> {
console.log('Connect to MongoDB');
})
.catch(e => {
console.log('Connect Error', e);
})
}
server.js와 연결하여 노드 실행 시 mongoose.connect도 함께 실행되도록 했습니다.
(...)
import { connect } from './models/index.js';
(...)
const app = express();
connect();
스키마를 만들 때는 mongoose 모듈의 Schema를 사용하여 정의합니다.
import mongoose from 'mongoose';
const { Schema } = mongoose;
const postSchema = new Schema({
title: String,
body: String,
tags: [String],
publishedDate : {
type: Date,
default: Date.now,
}
});
모델을 만들 때는 mongoose.model 함수를 사용합니다. ( model(스키마 이름, 스키마 객체) )
스키마 이름을 User라고 설정하면 실제 DB에 만드는 컬렉션 이름은 Users입니다.
이러한 컨벤션을 따르고 싶지않다면 model 함수의 세번째 파라미터에 원하는 이름을 작성하면 됩니다.
( ... )
// model 메서드로 스키마와 몽고디비 컬렉션을 연결하는 모듈을 만든다.
const User = mongoose.model('User', userSchema);
export default User;
스키마는 컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체입니다. 이와 달리 모델은 스키마를 사용하여 만드는 인스턴스로, DB에서 실제 작업을 처리할 수 있는 함수들을 가지고 있는 객체입니다.
REST API를 기반으로 api/posts/posts.ctrl.js에 블로그 포스터를 작성하는 API인 write를 구현했습니다.
import Post from '../../models/post.js'; // Post 모델 사용
export const write = async (req, res, next) => {
// 클라이언트에서 보낸 요청
const { title, body, tags } = req.body;
const post = new Post({
title,
tags,
body,
});
try{
await post.save(); // DB에 저장
res.json(post); // 클라이언트에 전달
// res.send(post);
}catch(e){
res.status(500); // 서버 오류
}
}
라우터를 설정해주고 Postman을 통해 서버가 응답한 결과를 잘 받았는지 확인했습니다.
데이터를 조회할 때는 모델 인스턴스의 find() 함수를 사용합니다.
find() 함수를 호출한 후에는 exec()를 붙여 주어야 서버에 쿼리를 요청합니다.
export const list = async (req, res, next) => {
try{
// Post 모델에서 데이터 조회
const posts = await Post.find().exec();
res.json(posts); // 클라이언트에 전달
}catch(e){
res.status(500).send(e);
}
}
특정 id 값을 가진 데이터를 조회할 때는 findById() 함수를 사용합니다.
id는 URL의 파라미터값으로 전달됨으로 req.params를 사용합니다.
export const read = async (req, res, next) => {
const { id } = req.params; // url 파라미터는 req.params 사용
try{
const post = await Post.findById(id).exec(); // id로 검색
if(!post){
res.status(404).send('포스트 없음');
return;
}
res.json(post); // 클라이언트에 전달
}catch(e){
res.status(500).send('서버 오류');
}
}
http://localhost:4000/posts/5e501cb13e527858a8aacc0
만약 문자열을 몇 개 제거하고 요청하면 500 오류가 발생합니다. 이는 전달받은 id가 ObjectId형태가 아니어서 발생하는 서버 오류입니다.
데이터를 삭제하는 함수는 여러 개가 있습니다. 그중 findByIdAndDelete()를 사용했습니다.
export const remove = async (req, res, next) => {
const { id } = req.params;
try{
await Post.findByIdAndDelete(id).exec(); // id를 찾아 삭제
res.status(204) // Noconnect (성공했지만 응답 데이터 없음)
}catch(e){
res.status(500).send(e);
}
}
데이터를 업데이트할 때는 findByIdAndUpdate() 함수를 사용합니다.
첫 번째 파라미터는 id, 두 번째 파라미터는 업데이트 내용, 세 번째 파라미터는 업데이트 옵션입니다.
export const update = async (req, res, next) => {
const { id } = req.params;
try {
const post = await Post.findByIdAndUpdate(id, req.body, {
new: true, // new 값이 ture이면 업데이트된 데이터를 반환.
// false 이면 업데이트 되기 전 데이터를 반환
})
if(!post){
res.status(404).send('포스터 없음');
}
res.json(post); // 클라이언트에 전달
} catch (e) {
res.satus(500).send(e);
}
}
server.js에 적용할 postRouter입니다.
import express from 'express';
import * as postCtrl from '../api/posts/posts.ctrl.js';
const router = express.Router();
// http://localhost:4000/posts/
router.post('/', postCtrl.write); // 작성
router.get('/', postCtrl.list); // 전체 조회
router.get('/:id', postCtrl.read); // 특정 포스터 읽기
router.delete('/:id', postCtrl.remove); // 포스터 삭제
router.patch('/:id', postCtrl.update); // 포스터 수정
export default router;
앞서 readAPI를 실행할 때, id가 올바른 ObjectId 형식이 아니라면 500 오류가 발생했습니다. 500 오류는 서버에서 처리하지 않아 내부적으러 문제가 생겼을 때 발생하는 것으로 지금 상황에서는 올바른 오류가 아닙니다.
잘못된 id가 전달했다면 클라이언트가 전달을 잘못 보낸것이므로 400 Bad Request 오류를 띄워주는 것이 맞습니다.
checkObjectId 미들웨어를 작성했습니다.
// api/posts/posts.ctrl.js
import mongoose from 'mongoose';
const { ObjectId } = mongoose.Types;
// ObjectId 검증 미들웨어
export const checkObjectId = (req, res, next) =>{
const { id } = req.params; // 클라이언트에서 보낸 아이디
if(!ObjectId.isValid(id)){
res.status(400).send('404 에러, 잘못된 아이디'); // Bad Reqeust
return;
}
return next(); // 다음 미들웨어 호출
}
라우터에서 ObjectId 검증이 필요한 부분에 checkObjectId 미들웨어를 추가합니다.
router.get('/:id', postCtrl.checkObjectId, postCtrl.read); // 특정 포스터 읽기
router.delete('/:id', postCtrl.checkObjectId, postCtrl.remove); // 포스터 삭제
router.patch('/:id', postCtrl.checkObjectId, postCtrl.update); // 포스터 수정
wirte, update API에서 전달받은 요청 내용을 검증하는 방법을 알아보겠습니다. 포스트를 작성할 때 title, body, tags 값을 모두 전달 받아야니다. 그리고 클라이언트가 값을 빼먹을때는 400 어류가 발생해야합니다.
Joi 라이브러리를 사용하여 요청 내용 검증을 해보겠습니다.
$ yarn add joi
import Joi from 'joi';
...
export const write = async (req, res, next) => {
// 객체가 다은 필드를 가지고 있음을 검증
const schema = Joi.object().keys({
title: Joi.string().required(),
body: Joi.string().required(),
tags: Joi.array().items(Joi.string()).required(),
});
const result = Joi.validate(req.body, schema);
if(result.error){
res.status(400).send(result.error);
return;
}
...
이제 API를 호출할 때 Request Body에 필요한 필드가 빠져있다면 서버는 400 오류를 응답합니다.
update API도 위와 같이 작성하여 req.body를 검증합니다.