Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
CH 22. mongoDB
- mongoDB
- mongoose
✅ 스키마란?
- 데이터베시으세 어떤 형식의 데이터를 넣을지에 대한 정보.
-> 회원 정보 스키마라면? 계정명, 이메일, 이름 등
데이터의 구조가 자주 바뀌면 - mongoDB가 유리
데이터를 필터링해야하거나, ACID 규칙을 지켜야 하면 - RDBMS가 유리
🔎 용어 설명
- ACID = 원자성/일관성/고립성/지속성
-> DB 트랜잭션이 안전하게 처리되는 것을 보장하기 위한 성질.- 트랜잭션 = 데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위.
= SELECT, UPDATE, INSERT, DELETE
{
"_id": ObjectID("4058293492938493f123a81"),
"username": "thisisyjin",
"name": { first: "Yeonjin", last: "Lee" }
}
-> 문서는 BSON (바이너리 형태의 JSON)로 저장됨.
새로운 문서를 만들면 _id
라는 고유값을 자동으로 생성함.
-> 시간 / 머신 아이디 / 프로세스 아이디 / 순차번호로 되어있음.
-> 즉, _id
는 고유한 값임.
여러 문서가 들어있는 곳을 컬렉션
이라고 함.
-> cf) RDBMS에서는 테이블 개념을 사용함. 각 테이블에서 같은 스키마를 가져야 함.
-> MongoDB에서는 다른 스키마를 가진 문서들이 한 컬렉션에 있을 수 있음.
// 컬렉션
// 문서
{
"_id": ObjectID("4058293492938493f123a81"),
"username": "thisis"
},
// 문서
{
"_id": ObjectID("405829cafdf8493f123f3f6"),
"username": "yjin",
"tel": "010-1234-1234"
},
-> 첫번째 문서와 두번째 문서의 스키마는 일치하지 않아도 OK.
{
_id: ObjectId,
title: String,
body: String,
username: String,
createdDate: Date,
comments: [
{
_id: ObjectID,
text: String,
createdDate: Date,
},
],
};
문서 내부에 또다른 문서가 위치 = 서브다큐먼트
macOS에서는 Homebrew로 설치 가능!
$ brew tap mongodb/brew $ brew install mongodb-community@4.2 $ brew services start mongodb-community@4.2 # Successfully started `mongodb-community@4.2`
참고 - 나는 여기서 설치가 제대로 되지 않아 (mongo 명령어 안됨)
아래와 같이 재설치해줌.$ brew tap mongodb/brew $ brew install mongodb-community@5.0 $ brew services start mongodb-community@5.0
http://localhost:27017/ 에 접속한 후
위와 같은 문구가 뜨면 제대로 작동하는 것임.
+) mongo 명령어 작성시 터미널 기반 mongoDB 클라이언트가 실행됨.
여기서 version()
이라는 명령어를 입력해보기.
-> 정상작동!
mongoose
는 Node.js 환경에서 MongoDB 기반 ODM(Object Data Modeling) 라이브러리임.mongoose
, dotenv
설치$ yarn add mongoose dotenv
참고로, 깃헙에 업로드시 .gitignore 을 추가하여 환경변수가 들어있는 파일은 제외시켜야 함.
.env
생성.PORT=4000
MONGO_URI=mongodb://localhost:27017/blog
-> 띄어쓰기 금지 / 세미콜론이나 컴마로 구분할 필요 X
mongodb://localhosst:27017/blog
에서 'blog'는 우리가 사용할 DB의 이름이다.
src/index.js 수정
dotenv
를 불러와서 config()함수 호출.// 🔻 최상단에서 config() 호출함.
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
// 🔻 process.env 내부 값에 대한 레퍼런스 (PORT)
const { PORT } = process.env;
const api = require('./api');
const app = new Koa();
const router = new Router();
router.use('/api', api.routes()); // api 라우트 적용
// 라우터 적용 전에 미들웨어를 적용해야 함
app.use(bodyParser());
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());
// 🔻 port값 설정
const port = PORT || 4000;
app.listen(port, () => {
console.log('Listening to port %d', port);
});
참고 : 자바스크립트에서
%d
란 ?
- 문자열 내부에서 %d나 %s 등을 사용하면 원하는 값 대입 가능.
(C언어의 printf와 같이)
=console.log('Listening to port ' + port)
와 같음.- 템플릿 리터럴의 경우 es6 문법이라 따로 바벨같은 트랜스파일러가 필요함.
.env 파일에서 PORT를 4001로 바꾼 후 서버를 재시작해보면 다음과 같다.
connect()
함수를 이용함.src/index.js 수정
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const mongoose = require('mongoose');
const api = require('./api');
// process.env 내부 값에 대한 레퍼런스
const { PORT, MONGO_URI } = process.env;
mongoose
.connect(MONGO_URI)
.then(() => {
console.log('Connected to MongoDB');
})
.catch((e) => {
console.error(e);
});
// 🔽 이하 동일
const app = new Koa();
const router = new Router();
router.use('/api', api.routes()); // api 라우트 적용
// 라우터 적용 전에 미들웨어를 적용해야 함
app.use(bodyParser());
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());
const port = PORT || 4000;
app.listen(port, () => {
console.log('Listening to port ' + port);
});
🔽 결과
-> esm 이라는 라이브러리를 사용하면 쉽게 import/export 를 사용할 수 있음.
$ yarn add esm
index.js
/* eslint-disable no-global-assign */
require = require('esm')(module /*, options*/);
module.exports = require('./main.js');
no-global-assign 이란, JS 예약어를 변수명으로 사용할 수 없는 옵션이다.
이를 무시하기 위해 주석을 추가해줘야 한다.
"scripts": {
"start": "node -r esm src",
"start:dev": "nodemon --watch src/ -r esm src/index.js"
}
-r esm src
를 추가해준다.
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
-> sourceType 옵션을 추가해줌.
exports
코드를 export const
로 변경해줌.module.exports
코드를 export default
로 변경해줌.require
코드를 import
로 변경해줌.단, main.js 에서
require('dotenv').config()
는 import로 바꾸지 ❌
전부 바꿨다면, postman으로 요청을 보내 에러가 없는것을 확인함.
프로젝트 루트 디렉터리(/blog-backend)에 jsconfig.json
파일을 생성.
{
"compilerOptions": {
"target": "es6",
"module": "es2015"
},
"include": ["src/**/*"]
}
-> 자동 완성을 통해 모듈을 불러올 수 있음.
vsCode에서는 import/export 문법 사용시
자동으로 import문을 불러온다. (require은 X)
mongoose에는 스키마(schema), 모델(model)이라는 개념이 있음.
스키마 | 모델 |
---|---|
문서 내부 필드가 어떤 형식인지 정의 | 스키마를 사용하여 만드는 인스턴스. 실제 작업을 처리할 수 있는 함수들을 지닌 객체. |
{ title: String, date: Date ... } | mongoose.model(...) |
DB는 모델(model)을 이용하여 데이터를 읽고 쓴다.
블로그 포스트의 경우에는 어떤 데이터가 필요할지?
필드명 | 데이터 타입 | 설명 |
---|---|---|
title | String | 제목 |
body | String | 본문 |
tags | ArrayOf(String) | 태그 |
publishedDate | Date | 작성일 |
src/models 디렉터리 생성 후, post.js
작성.
import mongoose from 'mongoose';
const { Schema } = mongoose;
const PostSchema = new Schema({
title: String,
body: String,
tags: [String],
publishedDate: {
type: Date,
default: Date.now,
},
});
Schema
를 이용하여 정의.필드의 기본값은 default
값을 설정해줌.
문자열로 된 배열 = [String] 과 같이 나타냄.
❗️ 참고 - Schema에서 지원하는 Type
- String / Number / Date / Buffer / Boolean / Mixed / ObjectId / Array
- Mixed는 어떤 데이터도 넣을 수 있는 형식. (
Schema.Types.Mixed
)- ObjectId는 객체 아이디. (주로 다른 객체 참조시)
- Array는 [ ]로 감싸서 사용.
예>
const BookSchema = new Schema({
title: String,
desc: String,
authors: [AuthorSchema], // 다른 객체를 배열로 참조함
meta: {
likes: Number,
},
extra: Schema.Types.Mixed
})
-> authors: [AuthorSchema]
는 Author스키마로 이루어진 여러개의 객체가 들어있는 배열을 의미함.
mongoose.model()
함수 이용post.js 수정
import mongoose from 'mongoose';
const { Schema } = mongoose;
const PostSchema = new Schema({
title: String,
body: String,
tags: [String],
publishedDate: {
type: Date,
default: Date.now,
},
});
// 🔻 추가
const Post = mongoose.model('Post', PostSchema);
export default Post;
컬렉션 명
- 스키마 명을
Post
로 정해주면 컬렉션 명은posts
가 된다.
-> 실제 DB에 만드는 컬렉션 이름을 posts로 지어줌.- 만약 위와 같이 컨벤션을 따르고 싶지 않다면, mongoose.model()의 세번째 인자로 원하는 이름을 넣어줌.
첫번째 인자인 Posts
는 다른 스키마에서 현재 스키마를 참조해야 하는 경우 사용함.
- 인스턴스를 만들면 바로 DB에 저장? (x)
->save()
함수를 실행해야 DB에 저장되는 것임.
-> save 함수의 리턴값은 Promise 이므로, await을 사용하여 대기할 수 있음.
참고 - async/await 문을 사용할 때는 try/catch 문으로 오류처리를 해줘야 함.
/src/api/posts/posts.ctrl.js 를 새로 작성.
import Post from '../../models/post';
export const write = async (ctx) => {
const { title, body, tags } = ctx.reqest.body;
const post = new Post({ title, body, tags });
try {
await post.save();
ctx.body = post;
} catch (e) {
ctx.throw(500, e);
}
};
... // 추후 작성
-> send를 누를때마다 _id
값이 다르게 나옴.
.env 파일에서 URI를
MONGO_URI=mongodb://localhost:27017/blog
로 해주었으므로 마지막에 'blog'가 DB의 이름이 된다.
-> 아까 POST로 등록했던 데이터들이 나타남.
export const list = async (ctx) => {
try {
const posts = await Post.find().exec();
ctx.body = posts;
} catch (e) {
ctx.throw(500, e);
}
};
참고 - mongoose의 find() 함수
Model.find() // 쿼리 인스턴스를 리턴 Model.find().exec() // 쿼리 이용시 Promise를 리턴받고 싶다면 - 즉, 서버에 쿼리 요청하려면
특정 id를 가진 데이터 조회시 findById()
함수 사용.
export const read = async (ctx) => {
const { id } = ctx.params;
try {
const post = await Post.findById(id).exec();
if (!post) { // 존재하지 않으면 undefined
ctx.status = 404;
return;
}
ctx.body = post;
} catch (e) {
ctx.throw(500, e);
}
};
삭제하려면 여러 종류의 함수를 이용할 수 있음!
이중에서 findByIdAndRemove()를 사용해보자.
export const remove = async (ctx) => {
const { id } = ctx.params;
try {
await Post.findByIdAndRemove(id).exec();
ctx.status = 204; // No Content
} catch (e) {
ctx.throw(500, e);
}
};
DELETE 요청으로 이미 삭제한 id를 GET으로 불러오려 하면, 404 NOT FOUND 오류가 뜬다.
findByIdAndUpdate() 함수를 사용함.
-> 세가지 파라미터 필요.
ctx.request.body
{ new: true }
: 업데이트된 데이터 반환. (false면 업데이트 이전 데이터 반환)export const update = async (ctx) => {
const { id } = ctx.params;
try {
const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
new: true, // 옵션 - 업데이트된 데이터 반환.
}).exec();
if (!post) {
ctx.status = 404;
return;
}
ctx.body = post;
} catch (e) {
ctx.throw(500, e);
}
};
postman에서 PATCH 요청을 해보자.
-> 변경할 필드만 작성하면 된다.