1. 소개하기
- 관계형 데이터베이스 한계
- 데이터 스키마 고정적
- 확장성(RDBMS는 처리해야 할 데이터양이 늘어나면 여러 컴퓨터에 분산시키는 것이 아니라, 해당 데이터베이스 서버의 성능을 업그레이트 해야 한다.)
- MongoDB는 이런 한계를 극복한 문서 지향적 NoSQL 데이터베이스
- 유동적 스키마
- 확장성(여러 컴퓨터로 분산하여 처리 가능하도록 쉽게 설계)
- MongoDB가 무조건 좋은 것은 하니다.
- 상황별로 적합한 데이터베이스가 다르다.
- 까다로운 조건으로 데이터를 필터링 또는 ACID특성을 지켜야 한다면 RDBMS가 유리
1.1. 문서란?
- 여기서 말하는 문서(document)는 RDBMS의 레코드와 개념이 비슷하다.
- 문서의 데이터 구조는 한 개 이상의 키-값 쌍으로 되어 있다.
문서의 예시
{
"id": ObjectId("47832749dsd"),
"username": "velopert",
"name": {first: "M.J.", last: "Kim"}
}
- 문서는 BSON(바이너리 형태의 JSON)형태로 저장된다.
- 그렇기 때문에 나중에 JSON형태의 객체를 데이터베이스에 저장할 때, 큰 공수를 들이지 않고도 데이터를 데이터베이스에 등록할 수 있어 매우 편하다.
- 새로운 문서를 만들면 _id라는 고육값을 자동으로 생성, 이 값은 시간, 머신 아디이, 프로세스 아이디, 순차 번호로 되어 있어 값의 고유함을 보장한다.
- 여러 문서가 들어있는 곳을 컬렉션이라 한다.
- 기존 RDBMS에서는 테이블 개념을 사용하므로 각 테이블마다 같은 스키마를 가지고 있어야 한다.
- 반면 MongoDB는 다른 스키마를 가지고 있는 문서들이 한 컬렉션에서 공존할 수 있다.
{
"id": ObjectId("47832749dsd"),
"username": "velopert",
},
{
"id": ObjectId("98sdas49dsd"),
"username": "velopert",
"phone": "010-1234-1234"
}
- 처음에 데이터에 전화번호가 필요 없었는데, 나중에는 필요해졌다고 가정해보자
- MongoDB에서는 컬렉션 안의 데이터가 같은 스키마를 가질 필요가 없으므로 그냥 넣어주면 된다.
1.2. MongoDB구조
1.3. 스키마 디자인
- MongoDB에서 스키마를 디자인하는 방식은 기존 RDBMS에서 스키마를 디자인하는 방식과 완전히 다르다.
- RDBMS에서 블로그용 데이터 스키마를 설계한다면 각 포스트, 댓글마다 테이블을 만들어 필요에 따라 JOIN에서 사용하는 것이 일반적
- 하지만 NoSQL에서는 그냥 모든 것을 문서 하나에 넣는다.
{
_id: ObjectId,
title: String,
body: String,
username: String,
createDate: Date,
comments: [
{
_id: ObjectId,
text: String,
createdDate: Date,
},
],
};
- 이런 상황에서 보통 MongoDB는 댓글을 포스트 문서 내부에 넣는다.
- 문서 내부에 또 다른 문서가 위치할 수 있는데, 이를 서브다큐먼트라고 한다.
- 서브다큐먼트 또한 일반 문서를 다루는 것처럼 쿼리할 수 있다.
- 문서 하나에는 최대 16MB만큼 데이터를 넣을 수 있다.
- 100자 댓글 데이터라면 대략 0.24KB를 차지한다.
- 16MB는 16,384KB이니 문서 하나에 댓글 데이터를 약 68,000개 넣을 수 있다.
- 서브다큐먼트에서 이 용량을 초과할 가능성이 있다면 컬렉션을 분리시키는 것이 좋다.
2. MongoDB 서버 준비
2.1. 설치
Windows
- https://www.mongodb.com/download-center/community 에서 인스톨러를 내려받아 설치한다.
2.2. MongoDB 작동 확인
3. mongoose의 설치 및 적용
- mongoose는 Node.js환경에서 사용하는 MongoDB 기반 ODM라이브러리이다.
- 이 라이브러리는 데이터베이스 문서들을 자바스크립트 객체처럼 사용할 수 있게 해준다.
- 이전에 만든 백엔드 프로젝트를 이어서 진행하겠다.
- $ yarn add mongoose dotenv
- dotenv는 환경변수들을 파일에 넣고 사용할 수 있게 하는 개발도구
- mongoose를 사용하여 MongoDB에 접속할 때, 서버에 주소나 계정 및 비밀번호가 필요할 경우도 있다.
- 이렇게 민감하거나 환경별로 달라질 수 있는 값은 코드 안에 직접 작성하지 않고, 환경변수로 설정하는 것이 좋다.
- 프로젝트를 깃허브 등에 올릴때는 .gitignore를 작성하여 환경변수가 들어 있는 파일은 제외시켜 주어야 한다.
3.1. .env 환경변수 파일 생성
- 환경변수에는 서버에서 사용할 포트와 MOngoDB 주소를 넣어주겠다.
[.env]
PORT=4000
MONGO_URI=mongodb://localhost:27017/blog
- 여기서 blog는 우리가 사용할 데이터베이스 이름이다.
- 지정한 데이터베이스가 서버에 없다면 자동으로 만들어 주므로 사전에 직접 생성할 필요는 없다.
- 다음으로 src/index.js파일의 맨 위에 다음과 같이 dotenv를 불러와서 config() 함수를 호출해 준다.
- Node.js에서 환경변수는 process.env값을 통해 조회할 수 있다.
[src/index.js]
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
// 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT } = process.env;
const api = require('./api');
const app = new Koa();
const router = new Router();
// 라우터 설정
router.use('/api', api.routes()); // api 라우터 적용
// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());
// PORT가 지정되어 있지 않다면 4000을 사용
const port = PORT || 4000;
app.listen(4000, () => {
console.log('Listening to port %d', port);
});
3.2. mongoose로 서버와 데이터베이스 연결
- mongoose를 이용해 서버와 데이터베이스를 연결하겠다.
- 연결할 때는 mongoose의 connect함수를 사용한다.
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, { useNewUrlParser: true, useFindAndModify: false })
.then(() => {
console.log('Connect to MongoDB');
})
.catch((e) => {
console.error(e);
});
const app = new Koa();
const router = new Router();
// 라우터 설정
router.use('/api', api.routes()); // api 라우터 적용
// 라우터 적용 전에 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 %d', port);
});
4. esm으로 ES모듈 import/export 문법 사용하기
- 기존 리액트 프로젝트에서 사용해 오던 ES모듈 import/export 문법은 Node.js에서 아직 정식으로 지원되지 않는다.
- Node.js에 해당 기능이 구현되어 있기는 하지만 아직 실험적인 단계이디 때문에 기본 옵션으로는 사용할 수 없다.
- 확장자를 .mjs로 사용하고 node를 실행할 때 --experimental-modules라는 옵션을 넣어 주어야 한다.
- Node.js에서 import/export 문법을 꼭 사용해야 할 필요는 없지만, 이 문법을 사용하면 VSCOde에서 자동 완성을 통해 모듈을 자동으로 쉽게 불러올 수 있고 코드도 더욱 깔끔해진다.
- 그래서 esm이라는 라이브러리의 도움을 받아 해당 문법을 사용해 보겠다.
- $ yarn add esm
- 기존 src/index.js 파일의 이름을 main.js로 변경하고, index.js 파일을 새로 생성해서 다음 코드를 작성한다.
[src/index.js]
// 이 파일에서만 no-global-assign ESLint 옵션을 비활성화한다.
/* eslint-disable no-global-assign */
require = require('esm')(module /*, options*/);
module.exports = require('./main.js');
package.json 코드 수정
[package.json]
(...)
"scripts": {
"start": "node -r esm src",
"start:dev": "nodemon --watch src/ -r esm src/index.js"
}
}
ESLint에서 import/export 구문을 사용해도 오류로 간주하지 않도록 다음과 같이 .eslintrc.json에서 sourceType 값을 "module"로 설정해준다.
[.eslintrc.json]
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "prettier"],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "warn",
"no-console": "off"
}
}
- 이제 프로젝트에서 import/export 구문을 자유롭게 사용할 수 있다.
- 서버 종료 뒤 다시 구동 yarn start:dev
4.1. 기존 코드 ES Module 형태로 바꾸기
[api/posts/posts/ctrl.js]
(...)
export const write = (ctx) => {
(...)
};
export const list = (ctx) => {
(...)
};
export const read = (ctx) => {
(...)
};
export const remove = (ctx) => {
(...)
};
export const replace = (ctx) => {
(...)
};
export const update = (ctx) => {
(...)
};
[src/api/posts/index.js]
import Router from 'koa-router';
import * as postCtrl from './post.ctrl';
const posts = new Router();
posts.get('/', postCtrl.list);
posts.post('/', postCtrl.write);
posts.get('/:id', postCtrl.read);
posts.delete('/:id', postCtrl.remove);
posts.put('/:id', postCtrl.replace);
posts.patch('/:id', postCtrl.update);
export default posts;
[src/api/index.js]
import Router from 'koa-router';
import posts from './posts';
const api = new Router();
api.use('/posts', posts.routes());
// 라우터를 내보낸다.
export default api;
[src/main.js]
require('dotenv').config();
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import mongoose from 'mongoose';
import api from './api';
// 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URI } = process.env;
(...)
[jsconfig.json]
{
"compilerOptions": {
"target": "es6",
"module": "es2015"
},
"include": ["src/**/*"]
}
- 위 코드와 같이 작성해주면 나중에 자동 완성을 통해 모듈을 불러올 수 있다.
5. 데이터베이스의 스키마와 모델
- mongoose에는 스키마와 모델이라는 개념이 있다.
- 이 둘은 혼동하기 쉽다.
- 스키마 : 컬렉션에 들어가는 문서 내부의 각 피드가 어떤 형식으로 되어 있는지 정의하는 객체
- 모델 : 스키마를 사용하여 만드는 인스턴스로, 데이터베이스에서 실제 작업을 처리할 수 있는 함수들을 지니고 있는 객체
5.1. 스키마 생성
- 모델을 만들려면 사전에 스키마를 만들어 주어야 한다.
- 블로그 포스트에 대한 스키마를 준비할 것이다. 어떤 데이터가 필요한지 한번 생각해보자
- 제목
- 내용
- 태그
- 작성일
- 포스트 하나에 총 네 가지 정보가 필요하다
- 각 정보에 대한 필드 이름과 데이터 타입을 설정하여 스키마를 만든다.
필드 이름 | 데이터 타입 | 설명 |
title | 문자열 | 제목 |
body | 문자열 | 내용 |
tags | 문자열 배열 | 태그 목록 |
publishedDate | 날짜 | 작성 날짜 |
- 스키마와 모델에 관련된 코드는 src/models 디렉터이레 작성한다.
- 디렉터리를 따로 만들어 관리하면 나중에 류지 보수를 편하게 할 수 있다.
[src/models/posts.js]
import mongoose from 'mongoose';
const { Schema } = mongoose;
const PostSchema = new Schema({
title: String,
body: String,
tags: [String], // 문자열로 이루어진 배열
publishedDate: {
type: Date,
default: Date.now, // 현재 날짜를 기본값으로 지정
},
});
- 스키마를 만들 때는 mongoose 모듈의 Schema를 사용하여 정의한다.
- 각 필드 이름과 필드의 데이터 타입 정보가 들어 있는 객체를 작성한다.
- 필드의 기본값으로는 default 값을 설정해주면 된다.
- Schema에서 지원하는 타입은 다음과 같다.
타입 | 설명 |
String | 문자열 |
Number | 숫자 |
Date | 날짜 |
Buffer | 파일을 담을 수 있는 버퍼 |
Boolean | true 또는 false값 |
Mixed(Schema, Types, Mixed) | 어떤 데이터도 넣을 수 있는 형식 |
ObjectId | 객체 아이디. 주로 다른 객체를 참조할 때 넣음 |
Array | 배열 형태의 값으로 [] 로 감싸서 사용 |
이 스키마를 활용하여 좀 더 복잡한 방식의 데이터도 저장할 수 있다.
[예시코드]
const AuthorSchema = new Schema({
name: String,
email: String,
});
const BookSchema = new Schema({
title: String,
description: String,
authors: [AuthorSchema],
meta: {
likes: Number,
},
extra: Schema.Types.Mixed,
});
- 위 코드에서 authors 부분에 [AuthorSchema]를 넣어주었다.
- 이는 Author 스키마로 이루어진 여러 개의 객체가 들어 있는 배열을 의미한다.
- 이렇게 스키마 내부에 다른 스키마를 내장시킬 수도 있다.
5.2. 모델 생성
- 모델을 만들 때는 mongoose.model 함수를 사용한다.
[src/models/posts.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;
- 모델 인스턴스를 만들고, export default 를 통해 내보내주었다.
- 여기서 사용한 model() 함수는 기본적으로 두 개의 파라미터가 필요하다.
- 첫 번째 파라미터 : 스키마 이름
- 두 번째 파라미터 : 스키마 객체
- 데이터베이스는 스키마 이름을 정해 주면 그 이름의 복수 형태로 데이터베이스에 컬렉션 이름을 만든다.
- 예를들어 스키마 이름을 Post로 설정하면, 실제 데이터베이스에 만드는 컬렉션 이름은 posts이다.
- BookInfo로 입력하면 bookinfos를 만든다.
- MongoDB에서 컬렉션 이름을 만들 때 권장되는 컨벤션은 구분자를 사용하지 않고 복수 형태로 사용하는 것이다.
- 이 컨벤션을 따르고 싶지 않다면, 다음 코드처럼 세 번째 파라미터에 원하는 이름을 입력하면 된다.
mongoose.model('Post', PostSchema, 'custom_book_collection');
- 이 경우 첫 번째 파라미터로 넣어 준 이름은 나중에 다른 스키마에서 현재 스키마를 참조해야 하는 상황에서 사용한다.
'Front > React' 카테고리의 다른 글
[React] JWT를 통한 회원 인증 시스템 구현 (0) | 2021.02.01 |
---|---|
[React] mongoose를 이용한 MongoDB 연동 실습2 (0) | 2021.01.29 |
[React] 백엔드 프로그래밍: Node.js의 Koa 프레임워크 (0) | 2021.01.25 |
[React] 서버 사이드 렌더링 (0) | 2021.01.24 |
[React] 코드 스플리팅 (0) | 2021.01.20 |