22. mongoose 이용 MongoDB 입문

히치키치·2022년 1월 10일
0

React_Advance

목록 보기
8/9
post-thumbnail

✔ MongoDB란

✔ MongoDB 서버 준비 & 적용

1. .env 환경변수 파일 생성

yarn add mongoose dotenv

dotenv를 통해 환경변수를 파일에 넣고 사용할 수 있음
mongoose 사용해 MongoDB 접속 시 필요한 계정/비밀번호, 환경에 따라 달라질 수 있는 값을 환경 변수로 설정해 코드 안에 직접 적지 않는다!!

.env

  • 환경 변수에 사용할 포트와 MongoDB 주소 넣기
PORT=4000
MONGO_URL=mongodb://localhost:27017/blog

src/index.js

  • dontev 불러와 config() 함수 호출
  • process.env 통해 Node.js에서 환경 변수 조회 가능
  • mongoose의 connect 함수를 통한 서버와 DB 연결
  • useFindAndModify : false는 버전 업그레이드로 인해 생략!!
require('dotenv').config();

const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');

const mongoose = require('mongoose');

//비구조화 할당으로 process.ev 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URL } = process.env;
mongoose
  .connect(MONGO_URL, { useNewUrlParser: true /*, useFindAndModify: false*/ })
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((e) => {
    console.log(e);
  });

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(port, () => {
  console.log('Listening to port %d', port);
});

2. mongoose로 서버와 데이터베이스 연결

  • mongoose의 connect 함수 이용
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.ev 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URL } = process.env;
mongoose
  .connect(MONGO_URL, { useNewUrlParser: true /*, useFindAndModify: false*/ })
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((e) => {
    console.log(e);
  });

✔ esm으로 ES 모듈 import/export 문법

  • .mjs 확장자 사용해 node 실행 시 --experimental-modules 옵션 부여 필수

1. esm 설치 & 환경 설정

yarn add esm

기존의 src/index.js 파일을 main.js로 변경 후 새로운 index.js 파일 작성

//이 파일에서만 no-global-assign ESLint 옵션 비활성화
/* eslint-disable no-global-assign*/

require = require('esm')(module /*,option*/);
module.exports = require('./main.js');

package.json 파일 수정

  "scripts": {
    "start": "node -r esm src",
    "start:dev": "nodemon --watch src/ src/index.js"
  }

ESLint에서 impott/export 구문 사용시 오류로 간주하지 않도록 .eslintrc.json에서 sourceType 값을 module로 설정

    "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType":"module"
    },

2. 기존 코드 ES Modules 형태로 바꾸기

api/posts/post.ctrl.js 파일에서 exports를 export const로 모두 변환

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 postsCtrl from './posts.ctrl';

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';

const api = new Router();

api.use('/posts', posts.routes());

//라우터 내보내기
export default api;

src/main.js

(...)
 //비구조화 할당으로 process.ev 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URL } = process.env;
(...)

jsconfig.json

{
    "compilerOptions": {
        "target": "es6",
        "module": "es2015"
    },
    "include": ["src/**/*"]
}

src/sample.js에서 api 입력 시 자동완성 여부 확인 후 삭제!

✔ 데이터베이스의 스키마와 모델

스키마
컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는 지 정의하는 객체

모델
스키마를 사용해 만드는 인스턴스
데이터베이스에서 실제 작업을 처리할 수 있는 함수 지닌 객체

1. 스키마 생성

블로그 포스트에 필요한 데이터

1. 제목 2. 내용 3. 태그 4. 작성일

각 정보에 대한 필드 이름과 데이터 타입 설정한 스키마

스키마 생성

import mongoose from 'mongoose';

const { Schema } = mongoose;

const PostSchema = new Schema({
  title: String,
  body: String,
  tags: [String], //문자열로 이루어진 배열
  publishedDate: {
    type: Date,
    default: Date.now, //현재 날짜를 기본값으로 지정
  },
});

📌 스키마에서 지원하는 타입

스키마 내부에 다른 스키마 내장 시키기 가능
[AuthorSchema] : Author 스키마로 이루어진 여러 개의 객체가 들어있는 배열 의미

const BookSchema=new Schema({
  (...)
  authors : [AuthorSchema]
  (...)
})

2. 모델 생성

src/models/post.js

(...)
 
 //모델 인스턴스 생성 후, export default를 통해 내보내기
const Post = mongoose.model('Post', PostSchema);
export default Post;

model( )
첫번째 인지 : 스키마 이름
두번째 인자 : 스키마 객체
스키마 이름의 복수형으로 DB에 컬렉션 이름 생성됨
ex ) 스키마 이름 : Post, 실제 DB에 만드는 컬렉션 이름 : posts

✔ 데이터 생성

1. 데이터 생성

src/api/posts/posts.ctrl.js

import Post from '../../models/post';

export const write = (ctx) => {};

export const list = (ctx) => {};

export const read = (ctx) => {};

export const remove = (ctx) => {};

export const update = (ctx) => {};

2. posts 라우트 작성

src/api/posts/index.js

import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl';

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', postsCtrl.remove);
posts.patch('/:id', postsCtrl.update);

export default posts;

3. 블로그 포스트 작성하는 write API 작성

  • post 인스턴스 생성
  • .save() 이용한 저장과 Promise 객체 반환

src/api/posts/posts.ctrl.js

import Post from '../../models/post';

/*
  POST /api/posts
  {
    title : "제목",
    body : "내용",
    tags : ["태그1","태그2", "태그3"]
  }
*/

export const write = async (ctx) => {
  const { title, body, tags } = ctx.request.body;

  const post = new Post({
    //포스트 인스턴스 만들기 위해 new 키워드 사용

    title,
    body,
    tags,
    //생성자 함수 파라미터로 정보 지닌 객체 넣기
  });

  try {
    await post.save();
    //save 함수 실행해 DB에 저장
    //save 함수는 Promise를 반환하기 때문에 async/await 문법 사용
    //데이터베이스 저장 요청 완료할 때까지 await 사용해 대기
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

(...)

4. Postman 요청/응답 확인


send 버튼 여러번 클릭 후 응답으로 나타나는 id 변경되는지 확인하기

5. MongoDB 확인

✔ 데이터 조회

1. 블로그 포스트 조회하는 list API 작성

  • find() 이용해 데이터 조회
  • .exec() 이용해 서버에 쿼리 요청
  • 조건 없이 모든 포스트 조회

src/api/posts/posts.ctrl.js

/*
 GET /api/posts
*/

export const list =async ctx =>{

  try{
    const posts=await Post.find().exec();
    ctx.body=posts;
  }
  catch(e){
    ctx.throw(500,e);
  }

};

2. Postman 요청/응답 확인

3. 특정 조건으로 포스트 조회하는 read API 작성

  • 특정 id에 해당하는 포스트 조회하는 read API
  • findById()를 통해 해당 id를 가진 데이터 조회
/* 
  GET /api/posts/:id
*/

export const read = async (ctx) => {
  const { id } = ctx.params;
  try {
    const post = await Post.findById(id).exec();
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

4. Postman 요청/응답 확인

유효한 id의 포스팅 조회 결과

존재하지 않는 id로 포스팅 조회 결과


id가 존재하지 않아 status로 404 오류 발생

유효하지 않는 id로 포스팅 조회 결과


문자열 몇 개 제거 후 요청시 500 오류 발생
전달받은 id가 ObjectId 형태가 아니여서 발생하는 오류

=> 검증 필요함!!

✔ 데이터 삭제

데이터 삭제 시 사용하는 함수
remove() : 특정 조건을 만족하는 데이터 모두 삭제
findByIdAndRemove() : id 찾아 삭제
findOneAndRemove() : 특정 조건 만족하는 데이터 하나 찾아 삭제

findOneAndRemove() 사용해 데이터 제거 진행

1. 해당 아이디의 포스팅 삭제하는 Remove API 작성

src/api/posts/posts.ctrl.js

(...)
/*
  DELETE /api/posts/:id
*/
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);
  }
}; 
(...)

2. Postman 요청/응답 확인

유효한 아이디로 포스트 삭제 요청
status 값으로 204 띄우며 삭제 성공했지만 응답하는 데이터 없는 상태

삭제 요청한 id의 포스팅 조회
status 값으로 404 나오며 삭제가 성공해 더이상 해당 아이디의 데이터가 조회되지 않음

✔ 데이터 수정

1. 해당 아이디의 포스팅 업데이트하는 Update API 작성

findByIdAndUpdate() 사용해 데이터 업데이트
첫번째 인자 : id
두번째 인자 : 업데이트 내용
세번째 인자 : 업데이트 옵션

src/api/posts/posts.ctrl.js

/*
  PATCH /api/posts/:id
  {
    title : "수정",
    body : "수정 내용",
    tags: ["수정", "태그"]
  }
*/
export const update = async (ctx) => {
  const { id } = ctx.params;
  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true,
      //이 값을 설정하면 업데이트된 데이터를 반환함
      //false일 때는 업데이트되기 전의 데이터 반환함
    }).exec();

    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

2. Postman 요청/응답 확인

✔ 요청 검증

1. ObjectId 검증

500 오류 : 서버에서 처리하지 않아 내부적으로 문제 생김
400 Bad Request : 잘못된 id 전달한 것은 클라이언트가 요청을 잘못 보낸 것임
ObjectId를 통해 id 값이 올바른 ObjectId인지 확인

import mongoose from "mongoose";
const {ObjectId}=mongoose.Types;
ObjectId.isValid(id);

read, remove, update 함수에 매번 검증 코드를 작성하지 않고
검증코드를 미들웨어로 만들어 여러 라우트에 적용!!

src/api/posts/posts.ctrl.js

import mongoose from 'mongoose';

//id 검증하는 미들웨어 작성
const { ObjectId } = mongoose.Types;

export const checkOjectId = (ctx, next) => {
  const { id } = ctx.params;
  if (!ObjectId.isValid(id)) {
    ctx.status = 404; //Bad Request
    return;
  }
  return next();
};

src/api/posts/index.js

ObjectId 검증이 필요한 부분에 미들웨어 추가

import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl';

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.checkOjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkOjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkOjectId, postsCtrl.update);

export default posts;

리팩토링하여 코드 재작성
api/posts/:id 경로를 위한 라우터 새로 작성하고 posts에 해당 라우터 등록

import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl';

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);

const post = new Router(); // /api/posts/:id
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', postsCtrl.remove);
posts.patch('/:id', postsCtrl.update);

posts.use('/:id', postsCtrl.checkOjectId, post.routes());

export default posts;

2. Request Body 검증

Joi 라이브러리 이용해 포스트 작성시 title, body, tags 값을 모두 전달 받도록 검증

Joi 라이브러리 설치

yarn add joi 

요청 내용 검증을 추가한 write API

기존 책의 코드 작성시 Joi.validate is not a function 오류 발생!!
다음 링크를 통해 코드 수정 통해 해결

src/api/posts/posts.ctrl.js

(...)
 import Joi from 'joi';
 (...)
/*
  POST /api/posts
  {
    title : "제목",
    body : "내용",
    tags : ["태그1","태그2", "태그3"]
  }
*/
export const write = async (ctx) => {
  const schema = Joi.object().keys({
    //객체가 다음 필드 가지고 있음 검증
    title: Joi.string().required(), //required()가 있으면 필수 항목
    body: Joi.string().required(),
    tags: Joi.array().items(Joi.string()).required(), //문자열로 이루어진 배열
  });

  //검증하고 나서 검증 실패인 경우 에러 처리
  const result = schema.validate(ctx.request.body);
  //const result = Joi.validate(ctx.request.body, schema);
  if (result.error) {
    ctx.status = 400; //Bad Request
    ctx.body = result.error;
    return;
  }

  const { title, body, tags } = ctx.request.body;

  const post = new Post({
    //포스트 인스턴스 만들기 위해 new 키워드 사용

    title,
    body,
    tags,
    //생성자 함수 파라미터로 정보 지닌 객체 넣기
  });

  try {
    await post.save();
    //save 함수 실행해 DB에 저장
    //save 함수는 Promise를 반환하기 때문에 async/await 문법 사용
    //데이터베이스 저장 요청 완료할 때까지 await 사용해 대기
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

Postman 요청/응답 확인
태그 없이 포스팅 POST 요청

다음과 같은 오류 반환

요청 내용 검증을 추가한 update API
ctx.request.body에 대한 검증 진행하지만 required()는 불필요

/*
  PATCH /api/posts/:id
  {
    title : "수정",
    body : "수정 내용",
    tags: ["수정", "태그"]
  }
*/
export const update = async (ctx) => {
  const { id } = ctx.params;
  //write에서 사용한 schema와 비슷한데 required()가 없음!!
  const schema = Joi.object().keys({
    title: Joi.string(),
    body: Joi.string(),
    tags: Joi.array().items(Joi.string()),
  });

  //검증하고 나서 검증 실패인 경우 에러 처리
  const result = schema.validate(ctx.request.body);
  if (result.error) {
    ctx.status = 400; //Bad Request
    ctx.body = result.error;
    return;
  }

  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true,
      //이 값을 설정하면 업데이트된 데이터를 반환함
      //false일 때는 업데이트되기 전의 데이터 반환함
    }).exec();

    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

Postman 요청/응답 확인

title을 문자열이 아닌 숫자로 POST 요청 시!!

string이 아니라는 에러 반환!!

✔ 페이지네이션 구현

1. 가짜 데이터 생성

200자 이상의 텍스트로 이루어진 body를 40개 포스트로 등록

src/createFakeData.js

import Post from './models/post';

export default function createFakeData() {
  //0,1, ... , 39로 이루어진 배열 생성 후 포스트 데이터로 변환
  const posts = [...Array(40).keys()].map((i) => ({
    title: `포스트#${i}`,
    //200자 이상의 텍스트
    body: 'Contrary to popular belief, Lorem Ipsum is not simply random text. \
        It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. \
        Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,\
        looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, \
        and going through the cites of the word in classical literature, discovered the undoubtable source. \
        Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil)\
        by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. \
        The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. \
        The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 \
        and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, \
        accompanied by English versions from the 1914 translation by H. Rackham.',
    tagss: ['가짜', '데이터'],
  }));
  Post.insertMany(posts, (err, docs) => {
    console.log(docs);
  });
}

2. main.js 통해 가짜 데이터 등록하기

src/main.js

(...)
 
import createFakeData from './createFakeData';
(...)
//비구조화 할당으로 process.ev 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URL } = process.env;

mongoose
  .connect(MONGO_URL, { useNewUrlParser: true /*, useFindAndModify: false*/ })
  .then(() => {
    console.log('Connected to MongoDB');
    createFakeData();
  })
  .catch((e) => {
    console.error(e);
  });

3. 콘솔창과 Compass를 통해 확인


4. 포스트 최근 순으로 불러오기

기존 list API에서는 포스트가 작성된 순서대로 나열됨

sort() 통해 가장 최근 작성된 포스트 먼저 나열
key 파라미터 : 정렬할 필드 설정하는 부분
key 값 : 1 : 오름차순, -1 : 내림차순

_id 내림차순 정렬하고자 하니 {_id:-1}로 설정

src/api/posts/posts.ctrl.js

(...)
 /*
 GET /api/posts
*/

export const list = async (ctx) => {
  try {
    const posts = await Post.find().sort({ _id: -1 }).exec();
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};
 
 (...)

5. 콘솔창과 Compass를 통해 확인

가장 마지막으로 등록된 포스트가 맨 위에 나타나는지 확인

6. 보이는 갯수 제한

limit() 사용해 한 번에 보이는 개수 제한
파라미터로 제한할 숫자 규정

한번에 10개를 보여 줄 거기 때문에 limit(10)

src/api/posts/posts.ctrl.js

/*
 GET /api/posts
*/

export const list = async (ctx) => {
  try {
    const posts = await Post.find().sort({ _id: -1 }).limit(10).exec();
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};

7. 페이지 기능 구현

skip() 사용해 몇 개의 데이터를 넘겨야 하는지 명시
(page-1)*n로 파라미터 설정해 한 페이지 당 n개의 데이터 불러옴
page값은 query에서 받아옴

src/api/posts/posts.ctrl.js


/*
 GET /api/posts
*/

export const list = async (ctx) => {
  //query는 문자열이기 때문에 숫자로 변환해 줘야 함
  //값이 주어지지 않으면 1을 기본으로 사용
  const page = parseInt(ctx.query.page || '1', 10);
  if (page < 1) {
    ctx.status = 400;
    return;
  }
  try {
    const posts = await Post.find()
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .exec();
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};

http://localhost:4000/api/posts?page=2 형태로 페이지 지정해 조회 가능

8. 마지막 페이지 번호 알려주기

커스텀 헤더 설정해 클라이언트에게 마지막 페이지 알려줌
Last-Page 커스텀 HTTP 헤더 설정

src/api/posts/posts.ctrl.js

/*
 GET /api/posts
*/

export const list = async (ctx) => {
  //query는 문자열이기 때문에 숫자로 변환해 줘야 함
  //값이 주어지지 않으면 1을 기본으로 사용
  const page = parseInt(ctx.query.page || '1', 10);
  if (page < 1) {
    ctx.status = 400;
    return;
  }
  try {
    const posts = await Post.find()
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .exec();

    const postCount = await Post.countDocuments().exec();
    ctx.set('Last-Page', Math.ceil(postCount / 10));
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};

9. Postman 통해 확인

10. 내용 길이 제한

body 길이가 200자 이상이면 뒤에 ... 붙이고 문자열 자름

  1. find()로 조회한 데이터는 mongoose 문서 인스턴스 형태로 toJSON() 함수로 JSON 형태로 변형
  2. lean()으로 데이터 조회해 처음부터 JSON 형태로 데이터 조회 가능

src/api/posts/posts.ctrl.js

/*
 GET /api/posts
*/

export const list = async (ctx) => {
  //query는 문자열이기 때문에 숫자로 변환해 줘야 함
  //값이 주어지지 않으면 1을 기본으로 사용
  const page = parseInt(ctx.query.page || '1', 10);
  if (page < 1) {
    ctx.status = 400;
    return;
  }
  try {
    const posts = await Post.find()
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .lean()
      .exec();

    const postCount = await Post.countDocuments().exec();
    ctx.set('Last-Page', Math.ceil(postCount / 10));
    ctx.body = posts.map((post) => ({
      ...post,
      body:
        post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
    }));
  } catch (e) {
    ctx.throw(500, e);
  }
};

11. Postman 통해 확인

0개의 댓글