TIL 22-05-16 (3)

thisisyjin·2022년 5월 16일
0

TIL 📝

목록 보기
51/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


CH 22. mongoDB + mongoose

  • 요청 검증 (ObjectId, Request body)
  • 페이지네이션 구현

request 검증

1) ObjectId 검증

  • 이전 글에서 작성한 read API를 실행할 때, id가 올바른 ObjectId 형식이 아닐 때는 500 error가 발생한다.
  • 500 error은 보통 서버 문제가 발생했을때이다.
    -> 잘못된 id를 전달했을 때는 400 (bad request)를 띄워주는것이 맞다.

위와 같은 이유로 id가 올바른 ObjectId인지 검사해야 한다.

import mongoose from 'mongoose';

const { ObjectId } = mongoose.Types;
ObjectId.isValid(id);
  • ObjectID를 검증해야 하는 API는 id를 조회하는 read / remove / update API 이다.

  • 코드를 중복해서 넣지 않고, 한 번만 구현한 후 여러 라우트에 쉽게 적용하려면?
    -> 미들웨어를 생성하면 됨!
    -> 코드 상단부에 생성 - 조건을 만족해야 next()로 다음 미들웨어로 넘어갈 수 있도록.

  1. posts/posts.ctrl.js 수정
import Post from '../../models/post';
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();
}

...

-> ObjectId.isValid(id)가 false면 400 에러를 발생시킴.

  • isValid()함수는 mongoose에서 제공하는 함수로, 값의 유효성을 체크할 수 있다.
    -> true/false를 반환함.
  1. posts/index.js 수정
import Router from 'koa-router';
import { list, write, read, remove, update, checkObjectId } from './posts.ctrl';

const posts = new Router();

posts.get('/', list);
posts.post('/', write);
// 🔻 checkObjectId 함수 추가 (두번째 인자로)
posts.get('/:id', checkObjectId, read);
posts.delete('/:id', checkObjectId, remove);
posts.patch('/:id', checkObjectId, update);

export default posts;

2-2. posts/index.js 리팩토링

import Router from 'koa-router';
import { list, write, read, remove, update, checkObjectId } from './posts.ctrl';

const posts = new Router();

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

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

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

export default posts;
  • /api/posts/:id 경로를 위한 라우터를 새로 만듬. (= post)
  • posts.use() 로 라우터 중첩 등록해줌.
    -> posts.use('/path', 라우트명.routes())
  • id를 일반 ObjectId의 길이와 다른 잘못된 id를 넣으면, 아래와 같이 400 에러가 발생함!

    (이전에는 500에러가 발생하던 것을 개선한 것)

2) Request Body 검증

  • write, update시에 전달받은 request 내용(body)을 검증하는 방법.
  • write API 에서는 title, body, tags를 모두 전달받아야 함.
  • 클라이언트가 필드 하나라도 빼먹으면 400 에러를 발생시켜야함.
  • 객체를 검증하기 위해서 if문을 쓸수 있지만, 더 쉽게 해주는 라이브러리인 Joi를 이용.

Joi 라이브러리

$ yarn add joi 
  1. write 함수 검증

posts.ctrl.js 수정

export const write = async (ctx) => {
  // 🔻 유효성 검사 (title,body,tags)
  const schema = Joi.object().keys({
    title: Joi.string().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;
    ctx.body = result.error;
    return;
  }

  // 🔽 이하 동일
  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);
  }
};

Joi 라이브러리는 객체의 각 필드의 값의 데이터타입 + 필수(required())를 설정할 수 있다.
-> 조건객체.validate(객체)를 해서 해당 객체가 조건을 만족하는지 얻을 수 있다.

  • Joi 공식 문서 읽어보기
  • 모든 데이터타입은 Joi.___()의 형태로 생겼다.
  • 검증 실패인 경우 {... error: 'username" is required'}와 같이 에러 필드를 포함한 객체를 반환한다.
    -> 위에서 result.error가 있는지 if문으로 걸러서 400 에러처리 해줌.
  1. update 함수 검증
  • update의 경우에는 required()를 제외하고 타입만 설정해주면 된다.
    (알아서 작성한 필드만 변경해주므로 전부 필수는 아님)
export const update = async (ctx) => {
  // 🔻 유효성 검사 (title,body,tags)
  const { id } = ctx.params;
  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;
    ctx.body = result.error;
    return;
  }

  // 🔽 이하 동일
  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true,
    }).exec();
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

✅ 참고 - Joi.object({})Joi.object().keys({})

  • keys() 를 붙이면 여러 키를 등록할 수 있음.
  • 만약 스키마를 하나만 정의할거면 Joi.object({})로 쓰고
    그렇지 않으면 (여러개의 키를 갖는다면) Joi.object().keys({})로 쓰기.
  • 참고 링크

  • title을 숫자형식으로 입력시 400 에러 발생.

페이지네이션

✅ 페이지네이션

  • 콘텐츠를 여러 개 페이지에 나눠서 보여주는 사용자 인터페이스.
  • 페이지 하단에 숫자가 나열된 것.
  • 블로그에서 포스트 목록은 한페이지당 10-20개 보이는 것이 적당하다.
  • (posts.ctrl.js의) list API를 수정해서 페이지네이션을 구현해보자.

가짜 데이터 생성

페이지네이션을 구현하기 위해서는 우선 데이터가 충분히 있어야 하므로, 가짜 데이터를 생성하는 js 파일을 만든다.

  • mongoose의 insertMany() 함수를 이용.

✅ insertMany()
The insertMany() function is used to insert multiple documents into a collection. It accepts an array of documents to insert into the collection.
-> 컬렉션에 많은 문서를 넣기 위한 함수임. (문서의 배열 형태로 된 값을 넣음)

  1. src/createFackeData.js 생성
import Post from './models/post';

export default function createFakeData() {
  const posts = [...Array(40).keys()].map((i) => ({
    title: `포스트 #${i}`,
    body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
    tags: ['fake', 'data'],
  }));

  Post.insertMany(posts, (err, docs) => {
    console.log(docs);
  });
}
  1. main.js에서 createFackeData 호출.
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';
// 🔻 임포트하기
import createFakeData from './createFakeData';

// process.env 내부 값에 대한 레퍼런스
const { PORT, MONGO_URI } = process.env;

mongoose
  .connect(MONGO_URI)
  .then(() => {
    console.log('Connected to MongoDB');
  // 🔻 mongoDB와 연결 후에, 가짜 데이터 만들기.
    createFakeData();
  })
  .catch((e) => {
    console.error(e);
  });

const app = new Koa();
const router = new Router();

router.use('/api', api.routes()); // api 라우트 적용

// 라우터 적용 전에 미들웨어를 적용해야 함
app.use(bodyParser());

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

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

이런식으로 터미널에 가짜 데이터의 정보가 출력된다. (console.log(docs)에 의해)
-> 총 40개의 글들이 저장된 것을 알 수 있다.

mongoDB compass에서 새로고침을 하면 posts에 40개의 데이터가 있음을 확인할 수 있다.
(참고로, 이전에 post했던 글들은 모두 delete 해줬음)

❗️ 주의

  • 데이터가 잘 생성된 것을 확인했으므로, main.js에서 createFakeData를 호출하는 코드를 삭제해준다.
    -> 이미 생성되었으므로 이제 필요없음!

1) 포스트 역순으로 불러오기

  • 가장 최근 작성된 포스트를 먼저 보여주기 위해.
  • list API 수정

posts.ctrl.js 수정

export const list = async (ctx) => {
  try {
    // 🔻 sort({ _id: -1 })을  .exec()  앞에 넣어줌.
    const posts = await Post.find().sort({ _id: -1 }).exec();
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};

-> exec()을 하기 전에 sort() 구문을 넣어준다. ( id를 기준으로 내림차순 정렬하도록 )
-> id가 클수록 최신에 작성한 글임. 즉, 큰값부터 정렬 = 내림차순.

⚡️ sort()

sort 함수의 파라미터는 { key: 1 } 또는 { key: -1 } 로 적어줌.

  • key는 정렬할 필드를 설정하는 것이고,
  • 1이면 오름차순 정렬 / -1이면 내림차순 정렬을 의미함.

2) 보이는 개수 제한

posts.ctrl.js 수정

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

-> limit() 함수로 개수를 제한할 수 있음.


3) 페이지 기능 구현

  • limit() 함수와 skip() 함수를 사용해야 함.
    -> skip(10)이면 처음 10개를 제외한 다음 데이터를 불러온다.
    -> 즉, skip 함수의 인자는 건너뛸 데이터의 개수이다.

  • skip()의 인자로 (page - 1) * 10 을 넣어주면 된다.
    -> page 값은 url의 쿼리에서 받아오도록 설정. (page 값이 없다면 1로 기본값)

posts.ctrl.js 수정

export const list = async (ctx) => {
  // 🔻 page 값은 query로 받아옴. (?page=2 와 같이) 
  const page = parseInt(ctx.query.page || '1');

  if (page < 1) {
    ctx.status = 400;
    return;
  }

  try {
    const posts = await Post.find()
      .sort({ _id: -1 })
      .limit(10)
    // 🔻 현재 1페이지면 0개 스킵, 2페이지면 10개 스킵, 3페이지면 20개 스킵 ...
      .skip((page - 1) * 10)
      .exec();
    
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};
  • 참고로, query는 문자열이므로 반드시 parseInt()를 해서 숫자형으로 바꿔줘야 함.

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

posts.ctrl.js 수정

export const list = async (ctx) => {
  const page = parseInt(ctx.query.page || '1');

  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.gst('Last-page', Math.ceil(postCount / 10)); // 페이지 수 카운트

    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};
  • Math.ceil은 올림(+1)을 하는 것.
  • 글이 38개라면 38/10 = 3.8 이고, 페이지는 3.8을 올림한 4개일 것임.

🔻 HTTP 헤더 설정

ctx.set('Last-page', Math.ceil(postCount / 10));

-> Last-page 라는 커스텀 헤더를 설정함.
postman을 통해 확인 가능.

-> 현재 총 40개의 글이 있으므로, 4페이지가 마지막 페이지임.


5) 글 내용 길이제한

posts.ctrl.js 수정

toJSON() 이용

export const list = async (ctx) => {
  const page = parseInt(ctx.query.page || '1');

  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)); // 페이지 수 카운트

    // 🔻 map()으로 우선 JSON 형식으로 배열을 바꿔준 후 변형해야 함.
    ctx.body = posts
      .map((post) => post.toJSON())
      .map((post) => ({
        ...post,
        body:
          post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
      }));
  } catch (e) {
    ctx.throw(500, e);
  }
};
  • find()를 통해 조회한 데이터는 Mongoose 문서 인스턴스 형태이므로, 데이터를 바로 변환할 수 ❌
    -> toJSON() 함수로 JSON 형태로 바꾼 다음에 변형해야 함.

lean() 이용

다른 방법으로, 데이터 조회시 lean() 함수를 이용할수도 있다.
.exec()의 앞에 .lean()을 넣어주면 처음부터 JSON 형식으로 데이터를 조회할 수 있다.

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

...

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


-> GET 요청을 해보면 이런식으로 200자로 잘 제한되어 있다.

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글