티스토리 뷰

책/리다기

리다기 정리13(몽고디비)

안양사람 2021. 2. 4. 01:22
728x90
SMALL

www.mongodb.com/try/download/community 에서 다운

 

yarn add mongoose dotenv

dotenv는 환경변수들을 파일에 넣고 사용할 수 있게 하는 개발도구입니다. mongoose를 사용하여 MongoDB에 접속할 때, 서버에 주소나 계정 및 비밀번호가 필요할 경우도 있습니다. 이렇게 민감하거나 환경별로 달라질 수 있는 값은 코드 안에 직접 작성하지 않고, 환경변수로 설정하는 것이 좋습니다. 프로젝트를 깃허브, 깃랩등의 서비스에 올릴 때는 .gigignore를 작성하여 환경변수가 들어 있는 파일은 제외시켜 주어야 합니다.

 

서버에서 사용할 포트와 MongoDB 주소. blog는 사용할 데이터베이스 이름(없으면 자동으로 생성)

.env

PORT=4000
MONGO_URL=mongodb://localhost:27017/blog

 

Node.js에서 환경변수는 process.env 값을 통해 조회할 수 있습니다.

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.use(router.routes()).use(router.allowedMethods());

// PORT가 지정되어 있지 않다면 4000을 사용
const port = PORT || 4000;
app.listen(port, () => {
  console.log('Listening to port %d', port);
});

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

 

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

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_URL } = process.env;

mongoose
  .connect(MONGO_URL, { userNewUrlParser: 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()); // api 라우트 적용

// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());

app.use(router.routes()).use(router.allowedMethods());

// PORT가 지정되어 있지 않다면 4000을 사용
const port = PORT || 4000;
app.listen(port, () => {
  console.log('Listening to port %d', port);
});

 

esm으로 ES 모듈 import/export 문법 사용하기

nodejs에서는 import/export 문법이 정식으로 지원되지 않아 그래서 사용하려면 esm설치

yarn add esm

src/index.js

// 이 파일에서만 no-global-assign ESLint 옵션을 비활성화 합니다.
/* eslint-disable no-global-assing */

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

package.json

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

.eslintrc.json

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

jsconfig.json(자동완성을 통해 모듈을 불러올 수 있어)

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

 

스키마 생성

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, // 현재 날짜를 기본값으로 지정
  },
});

const Post = mongoose.model('Post', PostSchema);
export default Post;

 

 

예시 코드(복잡)

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,
});

 

모델 생성

const Post = mongoose.mode('Post', PostSchema);
export default Post;

 

첫번째 피라미터 : 스키마 이름

두번째 피라미터 : 스키마 객체

데이터베이스는 스키마 이름을 정해주면 그 이름의 복수 형태로 데이터베이스에 컬렉션 이름을 만든다.

이 컨벤션을 따르고 싶지 않다면 세번째 파라미터에 원하는 이름 입력

이 경우 첫번째 파라미터로 넣어 준 이름은 나중에 다른 스키마에서 현재 스키마를 참조해야 하는 상황에서 사용

 

데이터 생성

save()로 저장

export const write = async (ctx) => {
  const { title, body, tags } = ctx.request.body;
  const post = new Post({
    title,
    body,
    tags,
  });
  try {
    await post.save();
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

 

데이터 조회

find()로 조회, exec()를 붙여야 서버에 쿼리를 요청

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

 

특정 포스트 조회

find대신 findById

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

 

데이터 삭제

remove() : 특정 조건을 만족하는 데이터를 모두 지웁니다.

findByIdAndRemove() : id를 찾아서 지웁니다.

findOneAndRemove() : 특정 조건을 만족하는 데이터 하나를 찾아서 제거합니다.

export const remove = async (ctx) => {
  const { id } = ctx.params;
  try {
    await (await Post.findByIdAndRemove(id)).execPopulate();
    ctx.stats = 204; // No Content (성공하기는 했지만 응답할 데이터는 없음)
  } catch (e) {
    ctx.throw(500, e);
  }
};

 

데이터 수정

findByIdAndUpdate() 함수 : 3가지 피라미터 -- id, 업데이트 내용, 업데이트의 옵션

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);
  }
};

 

요청 검증

ObjectId 검증

posts.ctrl.js

import mongoose from 'mongoose';

const { ObjectId } = mongoose.Types;

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

 

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.checkObjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkObjectId, postsCtrl.update);

export default 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
post.get('/', postsCtrl.read);
post.delete('/', postsCtrl.remove);
post.patch('/', postsCtrl.update);

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

 

Request Body 검증

yarn add @hapi/joi

posts.ctrl.js

import Joi from '@hapi/joi';

const { ObjectId } = mongoose.Types;

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

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);
  if(result.error){
    ctx.status=400; // Bad Request
    ctx.body=result.error;
    return
  }
  ~~~
  
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;
  }
  ~~~

 

포스트를 역순으로 불러오기

    const posts = await Post.find().sort({_id:-1}).exec();

보이는 개수 제한

    const posts = await Post.find().sort({ _id: -1 }).limit(10).exec();

페이지 기능 구현

skip : 넘긴다.

즉 (page-1)*10을 넣어 주면 1페이지에서는 처음 열 개, 2페이지에는 그다음 10개

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);
  }
};

 

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

응답 내용의 형식을 바꾸어 새롱누 필드를 설정하는 방법, Response 헤더 중 Link를 설정하는 방법, 커스텀 헤더를 설정하는 방법

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;

 

 

길이 내용 제한

1. 조회한 데이터는 mongoose 문서 인스턴스의 형태이므로 JSON 형태로 먼저 변환

    ctx.body = posts
      .map((post) => post.toJSON())
      .map((post) => ({
        ...post,
        body:
          post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
      }));

 

2. lean() 함수를 사용하면 처음부터 JSON 형태로 조회

    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)}...`,
      }));

 

 

 

 

 

728x90
LIST
댓글
공지사항