Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
CH 22. mongoDB + mongoose
- 요청 검증 (ObjectId, Request body)
- 페이지네이션 구현
위와 같은 이유로 id가 올바른 ObjectId인지 검사해야 한다.
import mongoose from 'mongoose';
const { ObjectId } = mongoose.Types;
ObjectId.isValid(id);
ObjectID를 검증해야 하는 API는 id를 조회하는 read / remove / update API 이다.
코드를 중복해서 넣지 않고, 한 번만 구현한 후 여러 라우트에 쉽게 적용하려면?
-> 미들웨어
를 생성하면 됨!
-> 코드 상단부에 생성 - 조건을 만족해야 next()
로 다음 미들웨어로 넘어갈 수 있도록.
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를 반환함.
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('/path', 라우트명.routes())
Joi
를 이용.$ yarn add joi
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 에러처리 해줌.
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({})
로 쓰기.- 참고 링크
✅ 페이지네이션
- 콘텐츠를 여러 개 페이지에 나눠서 보여주는 사용자 인터페이스.
- 페이지 하단에 숫자가 나열된 것.
페이지네이션을 구현하기 위해서는 우선 데이터가 충분히 있어야 하므로, 가짜 데이터를 생성하는 js 파일을 만든다.
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.
-> 컬렉션에 많은 문서를 넣기 위한 함수임. (문서의 배열 형태로 된 값을 넣음)
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);
});
}
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를 호출하는 코드를 삭제해준다.
-> 이미 생성되었으므로 이제 필요없음!
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이면 내림차순 정렬을 의미함.
posts.ctrl.js 수정
const posts = await Post.find().sort({ _id: -1 }).limit(10).exec();
-> limit()
함수로 개수를 제한할 수 있음.
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);
}
};
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);
}
};
ctx.set('Last-page', Math.ceil(postCount / 10));
-> Last-page 라는 커스텀 헤더를 설정함.
postman을 통해 확인 가능.
-> 현재 총 40개의 글이 있으므로, 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.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);
}
};
toJSON()
함수로 JSON 형태로 바꾼 다음에 변형해야 함.다른 방법으로, 데이터 조회시 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자로 잘 제한되어 있다.