2019. 9. 5. 10:59

1. mongodb , mongoose 모듈

1-1. mongodb

- Nosql 특성상 문서 객체 안에 들어간느 속성을 마음대로 바꿀 수 있으므로 매우 유연하게 데이터를 저장할 수 있다. 

- 하지만 컬렉션 안에 들어 있는 여러개의 객체를 조회할 때는 제약이 생길 수도 있음

ex) 같은 user 컬렉션 안에 들어 있는 문서 객체라도 어떤 문서객체는 age속성이 있는데, 다른 문서 객체에는 age속성이 없을 수 있음

-> 조회 조건을 공통 적용하기 어려움

 

1-2. mongoose

MongoDB 기반 ODM (object data modelling) 라이브러리 

- 스키마를 만들어, 이에 맞는정 자바스크립트 객체 그대로 db에 저장하거나 db의 문서 객체를 그대로 자바스크립트 객체로 바꿀 수 있게 함

 

2. mongoose 적용 

 

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

//.env
PORT=3000
MONGO_URI=mongodb://localhost:27017/blog

- MongoDB에 접속할 때, 서버주소, 포트번호, 계정, 비밀번호들의 민감하고 환경별로 달라지는 값은 코드에 작성하지 않고 환경변수로 설정하여 .env 환경변수 파일에 저장

- github등의 서비스에 올릴 때는 .gitignore를 작성하여 환경변수 파일 제외

- blog는 프로젝트에서 사용할 데이터베이스 이름

 

yarn add dotenv

 

//src/index.js
require('dotenv').config();

//...

const { PORT, MONGO_URI} = process.env;

//...

//웹 서버의 port속성 설정
http.createServer(app).listen(PORT, () => {
    console.log('express server starts : ' + PORT);
});

- dotenv는 환경변수들을 파일에 넣고 사용할 수 있게함

- require('dotenv').config() 호출 후 환경변수는 process.env값을 통해 조회 가능

 

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

//src/index.js

//...
const { PORT, MONGO_URI} = process.env;

mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
    console.log('connected to mongodb');
}).catch((e) => {
    console.error(e);
});

//...

 

2-3. import/export구문과 Node.js

- 리액트 프로젝트에서 쓰던 ES모듈 import/export 문법은 Node.js에서 정식으로 지원되지 않음

- require(), exports, module.exports 문법 사용

- Node.js v12부터는 정식 지원

 

3. 스키마와 모델

3.1 스키마

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

//src/models/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

 

3.2 모델

- 스키마를 사용하여 만드는 인스턴스

- 데이터베이스에서 실제 작업을 처리할 수 있는 함수들을 지니고 있는 객체

//src/modes/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

//모델 : 스키마를 사용하여 만든 인스턴스
//모델 생성 
module.exports = mongoose.model('Post', Post);

- mongoose.model()의 첫번째 파라미터 : 스키마 이름 -> 데이터베이스는 스키마 이름의 소문자, 복수형태로 데이터베이스에 컬련섹 생성

- mongoose.model()의 두번째 파라미터 : 스키마 객체

 

 

4. 데이터 생성, 조회, 삭제, 수정

4-1. 컨트롤러 수정

//src/api/posts/posts.ctrl.js
const Post = require('models/post');

//wrapAsync 메소드는 앞으로 async-await가 필요한 라우트 핸들러에 사용.
//try-catch를 이용해 상위 래퍼 메소드에서 에러 핸들링을 해주기 때문에 
//모든 메소드에 try-catch를 사용할 필요가 없어진다.
const wrapAsync = require('lib/wrapAsync');

express 에서 async/await 적용 : https://yongmin0000.tistory.com/category/Node.js/express

 

4-2. 데이터 생성

//src/api/posts/posts.ctrl.js
const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');

//데이터생성
//POST /api/posts
exports.write = wrapAsync(async (req, res, next) => {

    const { title, body, tags } = req.body;
    const post = new Post({
        title, body, tags
    });
    await post.save();  // 데이터베이스등록
    res.send(post);     //저장된 결과 반환
});

4-3. 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find().exec();
    
    res.send(posts);
});

4-4. 특정 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

//특정포스트 조회 
//GET /api/posts/:id 
exports.read = wrapAsync(async (req, res, next) => {
    const { id } = req.params;
    const post = await Post.findById(id).exec();
    //특정포스트 존재x
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터 조회 실패, 해당 id의 데이터 존재 x</h1>");
        return;
    }
    res.send(post);
});

 

4-5. 데이터 삭제

//src/api/posts/posts.ctrl.js

// ....

//데이터삭제
// DELETE /api/posts/:id
exports.remove = wrapAsync(async (req, res) => {
    const { id } = req.params;
    await Post.findByIdAndRemove(id).exec();
    console.log("DELETE 성공");
    res.status(204).send("<h1>삭제 성공</h1>");
});

 

4-6. 데이터 수정

//src/api/posts/posts.ctrl.js

// ....

//데이터 수정
//PATCH /api/posts/:id
exports.update = wrapAsync(async (req, res) => {
    const { id } = req.params;
    const post = await Post.findByIdAndUpdate(id, req.body, {
        new: true   //이 갑을 설정해야 업데이트 된 객체 반환, 설정안하면 업데이트 전 객체 반환
    }).exec();
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터수정실패, 해당 id의 데이터 존재x</h1>")
        return;
    }
    res.send(post);
}); 

 

5. 요청 검증

5.1 ObjectId

- id가 올바른 ObjectId형식인지 아닌지 검증함으로써 형식이 올바르지 않을 때 에러를 서버에서 처리할 수 있도록 한다

//src/api/posts/posts.ctrl.js

//...

//ObjectId 검증
const { ObjectId } = require('mongoose').Types;
exports.checkObjectId = (req, res, next) => {
    const { id } = req.params;

    //검증실패
    if (!ObjectId.isValid(id)) {
        console.log("ObjectId 실패")
        res.status(400).send("400 error : ObjectId 검증 실패");
        return null;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

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

module.exports = posts;

 

5.2 Request Body

- write, update API에서 전달받은 요청 내용에 title, body, tags값이 모두 들어있는지 검증

- 객체 검증 라이브러리 @hapi/joi 이용

yarn add @hapi/joi

 

//src/api/posts/posts.ctrl.js

const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');
const Joi = require('@hapi/joi');

//Request body 검증
exports.checkRequestBody = (req,res,next) => {

    //객체가 지닌 갑들을 검증
    const schema = Joi.object({
        title: Joi.string().required(),
        body: Joi.string().required(),
        tags:Joi.array().items(Joi.string()).required()
    });

    const result = schema.validate(req.body);
    //오류가 발생하면 오류내용응답
    if (result.error) {
        console.log("checkRequestBody 실패")
        res.status(400).send(result.error);
        return;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

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

module.exports = posts;

 

6. Pagination

6-1. 포스트 역순으로 불러오기 

- 최근 작성된 포스트를 먼저 보여주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .exec();
    
    res.send(posts);
});

 

6-2. 보이는 개수 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {


    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .exec();
    
    
    res.send(posts);
});

 

6-3. 페이지 기능 구현

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    res.send(posts);
});

 

6-4. 마지막 페이지 번호 알려 주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    res.send(posts);
});

 

6-5. 내용 길이 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .lean()             //반환형식을 JSON형태로 (내용길이 제한기능을 위해)
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    // 내용길이 제한
    const limitBodyLength = (post)=>({
        ...post,
        body:post.body.length <160 ? post.body : `${post.body.slice(0,160)}...`
    });
    res.send(posts.map(limitBodyLength));
});
Posted by yongminLEE