Mongoose를 통한 MongoDB 연동 실습

나혜수·2023년 4월 5일
0

리액트

목록 보기
22/23

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

관계형 DB의 한계

  • 스키마가 고정적
    데이터 형식이 기존의 데이터와 다르다면 기존 데이터를 모두 수정해야 새 데이터를 등록할 수 있다. 데이터 양이 많을 때 번거롭다.

    스키마는 DB의 구조와 제약 조건에 관한 전반적인 명세를 정의한 메타데이터의 집합이다. 자세히 말하면, 개체의 특성을 나타내는 속성과, 속성들의 집합으로 이루어진 개체, 개체 사이에 존재하는 관계에 대한 정의와 이들이 유지해야 할 제약 조건을 기술한 것이다.

  • 확장성
    저장하고 처리해야 할 데이터 양이 늘어나면 여러 컴퓨터에 분산시키는 것이 아니라 해당 데이터베이스 서버의 성능을 업그레이드하는 방식으로 확장해줘야 한다.


MongoDB

MongoDB는 위의 한계를 극복한 문서 지향적 NoSQL DB 이다. 현존하는 NoSQL DB 중에서 1위를 유지하고 있다.
NoSQLNot Only SQL 의 약자이다.

  • 데이터들은 유동적인 스키마를 지닐 수 있고, 새로 등록할 데이터의 형식이 바뀐다해도 기존 데이터를 수정할 필요가 없다.
  • 데이터 양이 늘어난다면 여러 컴퓨터에 분산하여 처리할 수 있도록 확장하기 쉽게 설계되었다.

문서

문서는 관계형 데이터 베이스의 record와 비슷한 개념이다. 문서의 데이터 구조는 1개 이상의 key-value 쌍으로 이뤄져있다.

📄 문서

  • 문서는 바이너리 JSON (BSON) 형태로 저장한다. JSON 형태의 객체를 DB에 등록할 때 매우 편하다.
  • 새로운 문서를 만들면 _id라는 고유값을 자동으로 생성한다.
{
    "_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 공식 홈페이지에서 설치파일을 다운로드한다.
MongoDB 기본 설치 경로 : C:\Program Files\MongoDB\Server\버전\bin\
터미널을 열어 해당 디렉토리로 이동하고 mongod 명령어를 입력해 서버를 실행한다.

Mongoose 설치 및 적용

// mongoose, dotenv 설치 
$ yarn add mongoose dotenv
  • Mongoose는 MongoDB 기반 ODM (Object Data Modelling) 라이브러리이다. 이 라이브러리는 데이터베이스 문서들을 JavaScript 객체처럼 사용할 수 있게 해준다.

  • dotenv는 환경변수들을 파일에 넣고 사용할 수 있게 해주는 개발도구이다. mongoose를 연결할 때 서버에 대한 계정과 비밀번호를 입력하게 되는데, 이런 민감한 정보는 코드에 직접 작성하지 않고 환경변수로 설정하는 것이 좋다.

.env 환경변수 파일 만들기

🏷️ .env
환경변수에는 서버에서 사용할 포트와 MongoDB 주소를 넣어준다. 프로젝트의 루트 경로에 .env 파일을 생성하여 다음과 같이 입력한다.

PORT=4000
MONGO_URI=mongodb://127.0.0.1:27017/blog

여기서 blog는 우리가 사용할 데이터베이스 이름이다. 지정한 데이터베이스가 서버에 없다면 자동으로 만들어주므로 사전에 따로 생성할 필요는 없다.

🏷️ src/index.js

  1. 파일의 맨 위에 다음과 같이 dotenv를 불러와서 config( ) 함수를 호출한다.
  2. Node.js에서 환경변수는 process.env 값을 통해 조회할 수 있다. 비구조화 할당을 통해 process.env 내부 값에 대한 레퍼런스를 만든다.
  3. port에는 기본값이 없으면 4000을 사용하도록 설정한다.
  4. 서버 포트를 설정하는 부분에 4000 대신 port 값을 넣어준다.
// 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를 이용하여 서버와 데이터베이스를 연결해보자. 연결할 때는 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를 사용하기 위한 준비를 마쳤다.


기존 코드 ES Module 형태로 바꾸기

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 에 요청을 보내 우리가 만든 서버가 오류 발생으로 종료되지 않고 잘 작동하는지 확인해보자.


DB의 스키마와 모델

mongoose에는 스키마 schema & 모델 model 개념이 있다.

  • 스키마는 컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체이다.
  • 모델은 스키마를 사용하여 만드는 인스턴스로, 데이터베이스에서 실제 작업을 처리할 수 있는 함수들을 지니고 있는 객체이다.

스키마 생성

우리는 블로그 포스트에 대한 스키마를 생성할 것이다.

  • 제목, 내용, 태그, 작성일

포스트 하나에 이렇게 총 4가지 정보가 필요하다. 각 정보에 대한 필드 이름과 데이터 타입을 설정한다.

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

profile
오늘도 신나개 🐶

0개의 댓글