1. JWT의 이해
- jwt은 JSON Web Token의 약자로, 데이터가 JSON으로 이루어져 있는 토큰을 의미한다.
- 두 개체가 서로 안전하게 정보를 주고받을 수 있도록 웹 표준으로 정의된 기술
1.1. 세션 기반 인증과 토큰 기반 인증의 차이
[세션 기반 인증]
- 서버가 사용자가 로그인 중임을 기억하고 있다는 뜻
- 서버는 세션 저장소에 사용자의 정보를 조회하고 세션id를 발급한다.
- 발급된 id는 주로 브라우저의 쿠키에 저장
- 그 다음 사용자가 다른 요청을 보낼 때마다 서버는 세션 저장소에서 세션을 조회한 후 로그인 여부를 결정하여 작업을 처리하고 응답한다.
- 세션 저장소는 주로 메모리, 디스크, 데이터베이스 등을 사용한다.
- 단점
- 서버를 확장하기 번거로워질 수 있다.
- 만약 서버의 인스턴스가 여러 개 된다면, 모든 서버끼리 같은 세션을 공유해야 하므로 세션 전용 데이터베이스를 만들어야 할 뿐 아니라 신경써야 할 것도 많다.
[토큰 기반 인증]
- 토큰은 로그인 이후 서버가 만들어 주는 문자열이다.
- 해당 문자열 안에는 사용자의 로그인 정보가 들어있고, 해당 정보가 서버에서 발급되었음을 증명하는 서명이 들어있다.
- 서명 데이터는 해싱 알고리즘을 통해 만들어지는데, 주로 HMAC SHA256 혹인 RSA SHA256알고리즘이 사용된다.
- 서버에서 만들어 준 토큰은 서명이 있기 때문에 무결성이 보장된다.(무결성이란 : 정보가 변경되거나 위조되지 않았음을 의미하는 성질)
- 사용자가 로그인을 하면 서버에서 사용자에게 해당 사용자의 정보를 지니고 있는 토큰을 발급해 주고, 추후 사용자가 다른 API를 요청하게 될 때 발급받은 토큰과 함께 요청하게 된다.
- 그러면 서버는 해당 토큰이 유효한지 검사하고, 결과에 따라 작업을 처리하고 응답한다.
- 장점
- 서버에서 사용자 로그인 정보를 기억하기 위해 사용하는 리소스가 적다
- 사용자 쪽에서 로그인 상태를 지닌 토큰을 가지고 있으므로 서버의 확장성이 매우 높다.
- 서버의 인스턴스가 여러 개로 늘어나도 서버끼리 사용자의 로그인 상태를 공유하고 있을 필요가 없다.
2. User 스키마/모델 만들기
- User스키마와 모델을 작성하여 사용자의 정보를 MongoDB에 담고 조회해보자
- 앞으로 만들 사용자 스키마에는 사용자 계정명과 비밀번호가 필요하다.
- 비밀번호를 데이터베이스에 저장할 때 플레인 텍스트로 저장하면 보안상 매우 위험하다.
- 따라서 단방향 해싱 함수를 지원해 주는 bcrypt라는 라이브러리를 사용하여 비밀번호를 안정하게 저장한다.
[src/modules/user.js]
import mongoose, { Schema } from 'mongoose';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
const User = mongoose.model('User', UserSchema);
export default User;
$ yarn add bcrypt
2.1. 모델 메서드 만들기
모델에서 사용할 수 있는 함수를 의미하며, 두 가지 종류가 있다.
1. 인스턴스 메서드 : 모델을 통해 만든 문서 인스턴스에서 사용할 수 있는 함수를 의미
const user = new User({username: 'velopert'});
user.setPassword('mypass123');
2. 스태틱 메서드 : 모델에서 바로 사용할 수 있는 함수
const user = User.findByUsername('velopert');
- 두 개의 인스턴스 메서드를 만들어 보겠다.
- 첫 번째 메서드는 setPassword이다.
- 비밀번호를 파라미터로 받아서 계정의 hashedPassword값을 설정해 준다.
- 두 번째 메서드는 checkPassword이다.
- 파라마터로 받은 비밀번호가 해당 계정의 비밀번호와 일치라는지 검증해준다.
[src/module/user.js]
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result; // true / false
};
const User = mongoose.model('User', UserSchema);
export default User;
- 인스턴스 메서드를 작성할 때는 화살표 함수가 아닌 function 키워드를 사용하여 구현해야 한다.
- 함수 내부에서 this에 접근해야 하기 때문에
- 여기서 this는 문서 인스턴스를 가리킨다.
- 화살표 함수를 사용하면 this는 문서 인스턴스를 가리키지 못하게 된다.
2.2. 스태틱 메서드 만들기
findByUsername이라는 메서드를 작성함으로써 username으로 데이터를 찾을 수 있게 해준다.
[src/module/user.js]
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result; // true / false
};
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
const User = mongoose.model('User', UserSchema);
export default User;
- 스태틱 함수에서의 this는 모델을 가리킨다.(User)
3. 회원 인증 API 만들기
새로운 라우트 정의
[src/api/auth/auth.ctrl.js]
export const register = async (ctx) => {
// 회원가입
};
export const login = async (ctx) => {
// 로그인
};
export const check = async (ctx) => {
// 로그인 상태 확인
};
export const logout = async (ctx) => {
// 로그아웃
};
auth 디렉터리에 index.js 파일을 만들어 auth 라우터를 생성한다.
[src/api/auth/index.js]
import Router from 'koa-router';
import * as authCtrl from './auth.ctrl';
const auth = new Router();
auth.post('/register', authCtrl.register);
auth.post('/login', authCtrl.login);
auth.get('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);
export default auth;
그 다음 auth 라우터를 api 라우터에 적용시킨다.
[src/api/index.js]
import Router from 'koa-router';
import posts from './posts';
import auth from './auth';
const api = new Router();
api.use('/posts', posts.routes());
api.use('/auth', auth.routes());
// 라우터를 내보낸다.
export default api;
3.1. 회원가입 구현하기
[src/api/auth/auth.ctrl.js]
import Joi from '@hapi/joi';
import User from '../../models/user';
/*
POST /api/auth/register
{
username: 'velopert',
password: 'mypass123'
}
*/
export const register = async (ctx) => {
// Request Body 검증하기
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = schema.validate(ctx.request.body);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
}
const { username, password } = ctx.request.body;
try {
// username이 이미 존재하는지 확인
const exists = await User.findByUsername(username);
if (exists) {
ctx.status = 409; // Conflict
return;
}
const user = new User({
username,
});
await user.setPassword(password); // 비밀번호 설정
await user.save(); // 데이터베이스에 저장
// 응답할 데이터에서 hashedPassword 필드 제거
const data = user.toJSON();
delete data.hashedPassword;
ctx.body = data;
} catch (e) {
ctx.throw(500, e);
}
};
- 회원가입 할 때 중복되는 계정이 생기지 않도록 기존에 해당 username이 존재하는지 확인(findByUsername)
- 비밀번호를 설정하는 과정에서 setPassword 인스턴스 함수를 사용
- 스태틱 또는 인스턴스 함수에서 해야 하는 작업들은 이 API 함수 내부에서 직접 구현해도 상관없지만, 이렇게 메서드들을 만들어서 사용하면 가독성도 좋고 추후 유지 보수를 할 때도 도움이 된다.
- 함수의 마지막 부분에서 hashedPassword필드가 응답되지 않도록 데이터를 JSON으로 변환한 후 delete를 통해 해당 필드를 지워주었다. 앞으로 비슷한 작업을 자주 하게 될 것이다. 따라서 이 작업을 selialize라는 인스턴스 함수로 따로 만들어 주겠다.
[src/modules/user.js] - serialize
UserSchema.methods.serialize = function () {
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
이제 기존의 코드를 serialize()로 대체시킨다.
[src/api/auth/auth.ctrl.js] - register
export const register = async (ctx) => {
(...)
const user = new User({
username,
});
await user.setPassword(password); // 비밀번호 설정
await user.save(); // 데이터베이스에 저장
// 응답할 데이터에서 hashedPassword 필드 제거
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
[결과]
3.2. 로그인 구현하기
[src/api/auth/auth.ctrl.js] - login
/*
POST /api/auth/login
{
username: 'velopert',
password: 'mypass123'
}
*/
export const login = async (ctx) => {
const { username, password } = ctx.request.body;
// username, password가 없으면 에러 처리
if (!username || !password) {
ctx.status = 401; // Unauthorized
return;
}
try {
const user = await User.findByUsername(username);
// 계정이 존재하지 않으면 에러 처리
if (!user) {
ctx.status = 401;
return;
}
const valid = await user.checkPassword(password);
console.log(valid);
// 잘못된 비밀번호
if (!valid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
} catch (e) {
ctx.throw(500, e);
}
};
- 이 API에서는 username, password 값이 제대로 전달되지 않으면 에러로 처리한다.
- 그리고 findByUsername을 통해 사용자 데이터를 찾고, 만약 사용자 데이터가 없으면 역시 에러로 처리한다.
- 계정이 유효하다면 checkPassword를 통해 비밀번호를 검사하고 성공했을 때는 계정 정보를 응답한다.
[결과]
4. 토큰 발급 및 검증하기
JWT 토큰을 만들기 위해 jsonwebtoken이라는 모듈을 설치해야 한다.
$ yarn add jsonwebtoken
4.1. 비밀키 설정
[.env]
PORT=4000
MONGO_URI=mongodb://localhost:27017/blog
JWT_SECRET=qwer1234
- 설정한 비밀키는 외부에 공개되면 절대 안된다.
- 비밀키가 공개되는 순간, 누구든지 마음대로 JWT토큰을 발급할 수 있기 때문
4.2. 토큰 발급하기
[src/modules/user.js] - generateToken
import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
(...)
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
// 첫 번째 파라미터에는 토큰 안에 집어넣고 싶은 데이터를 넣는다.
{
_id: this.id,
username: this.username,
},
process.env.JWT_SECRET, // 두 번째 파라미터에는 JWT 암호를 넣는다.
{
expiresIn: '7d', // 7일 동안 유효함
},
);
return token;
};
- 회원가입과 로그인에 성공했을 때 토큰을 사용자에게 전달해 준다.
- 사용자가 브라우저에서 토큰을 사용할 때는 주로 두 가지 방법을 사용한다.
- 첫 번째 : 브라우저의 localStorage혹은 sessionStorage에 담아서 사용하는 방법
- 사용하기 매우 편리하고 구현하기 쉽다
- 하지만 만약 누군가가 페이지에 악성 스크립트를 삽입한다면 쉽게 토큰을 탈취할 수 있다.(XSS공격)
- 두 번째 : 브라우저의 쿠키에 담아서 사용하는 방법
- 쿠키에 담아도 같은 문제가 발생할 수 있다.
- 하지만 httpOnly라는 속성을 활성화하면 자바스크립트를 통해 쿠키를 조회할 수 없으므로 악성 스크립트로부터 안전하다
- 대신 CSRF라는 공격에 취약해질 수 있다.
- 이 공격은 토큰을 쿠키에 담으면 사용자가 서버로 요청을 할 때마다 무조건 토큰이 함께 전달되는 점을 이용해서 사용자가 모르게 원하지 않는 API요청을 하게 만든다.
- 예를들어 사용자가 자신도 모르는 상황에서 어떠한 글을 작성하거나 삭제하거나 또는 탈퇴하게 만들 수 있다.
- 단, CSRF는 CSRF토큰 사용 및 Refer 검증 등의 방식으로 제대로 막을 수 있는 반면, XSS는 보안장치를 적용해 놓아도 개발자가 놓칠 수 있는 다양한 취약점을 통해 공격을 받을 수 있다.
- 여기서는 사용자 토큰을 쿠키에 담아서 사용하겠다.
[src/api/auth/auth.ctrl.js] - register, login
export const register = async (ctx) => {
(...)
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
export const login = async (ctx) => {
(...)
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
4.3. 토큰 검증하기
사용자의 토큰을 확인한 후 검증하는 작업을 미들웨어를 통해 처리해본다.
[src/lib/jwtMiddleware.js]
import jwt from 'jsonwebtoken';
const jwtMiddleware = (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) return next(); // 토큰이 없음
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded);
return next();
} catch (e) {
// 토큰 검증 실패
return next();
}
};
export default jwtMiddleware;
- 미들웨어를 만든 뒤 main.js에서 app에 미들웨어를 적용해본다.
- jwtMiddleware를 적용하는 작업은 app에 router미들웨어를 적용하기 전에 이루어져야 한다.(즉, 코드가 더욱 상단에 위치해야한다.)
[src/main.js]
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 jwtMiddleware from './lib/jwtMiddleware';
(...)
const app = new Koa();
const router = new Router();
// 라우터 설정
router.use('/api', api.routes()); // api 라우터 적용
// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());
app.use(jwtMiddleware);
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());
이렇게 해석된 결과를 이후 미들웨어에서 사용할 수 있게 하려면 ctx의 state안에 넣어 주면 된다.
[src/lib/jwtMiddleware.js]
import jwt from 'jsonwebtoken';
const jwtMiddleware = (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) return next(); // 토큰이 없음
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
console.log(decoded);
return next();
} catch (e) {
// 토큰 검증 실패
return next();
}
};
export default jwtMiddleware;
이제 check함수를 구현한다.
[src/api/auth/auth.ctrl.js] - check
/*
GET /api/auth/check
*/
export const check = async (ctx) => {
const { user } = ctx.state;
if (!user) {
// 로그인 중 아님
ctx.status = 401; // Unauthorized
return;
}
ctx.body = user;
};
[결과]
4.4. 토큰 재발급하기
- iat : 토큰이 언제 만들어졋는지 알려 주는 값
- exp : 언제 만료되는지 알려주는 값
exp에 표현된 날짜가 3.5일 미만이라면 토큰을 재발급해 주는 기능을 구현해 보자
[src/lib/jwtMiddleware.js]
import jwt from 'jsonwebtoken';
import User from '../models/user';
const jwtMiddleware = async (ctx, next) => {
const token = ctx.cookies.get('access_token');
if (!token) return next(); // 토큰이 없음
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
// 토큰의 남은 유효기간이 3.5 일 미만이면 재발급
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 60 * 60 * 24 * 3.5) {
const user = await User.findById(decoded._id);
const token = user.generateToken();
ctx.cookies.set('access_token', token, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
httpOnly: true,
});
}
return next();
} catch (e) {
// 토큰 검증 실패
return next();
}
};
export default jwtMiddleware;
[src/modules/user.js] - generateToken
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
// 첫 번째 파라미터에는 토큰 안에 집어넣고 싶은 데이터를 넣는다.
{
_id: this.id,
username: this.username,
},
process.env.JWT_SECRET, // 두 번째 파라미터에는 JWT 암호를 넣는다.
{
expiresIn: '3d', // 7일 동안 유효함
},
);
return token;
};
[결과]
토큰이 재발급된다.
재발급 된것이 확인되면 토큰 유효기간을 다시 7일로 돌린다.
4.5. 로그아웃 기능 구현하기
쿠키를 지워주면 된다. 간단하다.
[src/api/auth/auth.ctrl.js] - logout
/*
POST /api/auth/logout
*/
export const logout = async (ctx) => {
ctx.cookies.set('access_token');
ctx.status = 204; // No Content
};
[결과]
access_token이 비워지는 Set-Cookie 헤더가 나타난다.
5. posts API에 회원 인증 시스템 도입하기
5.1. 스키마 수정하기
Post스키마 안에 사용자의 id와 username을 전부 넣어 주어야 한다.
[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, // 현재 날짜를 기본값으로 지정
},
user: {
_id: mongoose.Types.ObjectId,
username: String,
},
});
const Post = mongoose.model('Post', PostSchema);
export default Post;
5.2. posts 컬렉션 비우기
- 이제 포스트 데이터는 사용자 정보가 필요하다.
- 이전에 만들어뒀던 데이터를 삭제한다.
5.3. 로그인했을 때만 API를 사용할 수 있게 하기
checkLoggedId이라는 미들웨엉를 만들어서 로그인해야만 글쓰기, 수정, 삭제를 할 수 있도록 구현한다.
[src/lib/checkLoggedIn.js]
const checkLoggedIn = (ctx, next) => {
if (!ctx.state.user) {
ctx.status = 401; // Unauthorized
return;
}
return next();
};
export default checkLoggedIn;
- 이 미들웨어는 로그인 상태가 아니라면 401 HTTP status를 반환한다.
- 그렇지 않으면 다음 미들웨어들을 실행한다.
- 이제 이 미들웨어를 posts라우터에서 사용해 보겠다.
[src/api/posts/index.js]
import Router from 'koa-router';
import * as postsCtrl from './post.ctrl';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router(); // /api/posts/:id
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.update);
posts.use('/:id', postsCtrl.checkObjectId, post.routes());
export default posts;
5.4. 포스트 작성 시 사용자 정보 넣기
로그인된 사용자만 포스트를 작성할 수 있게 했으니, 지금부터 포스트를 작성할 때 사용자 정보를 넣어서 데이터베이스에 저장하도록 구현해보겠다.
[src/api/posts/posts.ctrl.js]
export const write = async (ctx) => {
(...)
const { title, body, tags } = ctx.request.body;
const post = new Post({
title,
body,
tags,
user: ctx.state.user,
});
try {
await post.save();
ctx.body = post;
} catch (e) {
ctx.throw(500, e);
}
};
[결과]
5.5. 포스트 수정 및 삭제 시 권한 확인하기
- 이 작업을 미들웨어에서 처리하고 싶다면 id로 포스트를 조회하는 작업도 미들웨어로 해 주어야 한다.
- 따라서 기존에 만들었던 checkObjectId를 getPostById로 바꾸고, 해당 미들웨어에서 id로 포스트를 찾은 후 ctx.state에 담아 주겠다.
[src/api/posts/posts.ctrl.js] - getPostById (기존 checkObjectId)
export const getPostById = async (ctx, next) => {
const { id } = ctx.params;
if (!ObjectId.isValid(id)) {
ctx.status = 400; // Bad Request
return;
}
try {
const post = await Post.findById(id);
// 포스트가 존재하지 않을 때
if (!post) {
ctx.state = 404; // Not Found
return;
}
ctx.state.post = post;
return next();
} catch (e) {
ctx.throw(500, e);
}
};
[src/api/posts/index.js]
(...)
posts.use('/:id', postsCtrl.getPostById, post.routes());
export default posts;
[src/api/posts/posts.ctrl.js] - read
export const read = async (ctx) => {
ctx.body = ctx.state.post;
};
이번에는 checkOwnPost라는 미들웨어를 만든다.
이 미들웨어는 id로 찾은 포스트가 로그인 중인 사용자가 작성한 포스트인지 확인해준다.
만약 사용자의 포스트가 아니라면 403에러를 발생시킨다.
[src/api/posts/posts.ctrl.js] - checkOwnPost
export const checkOwnPost = (ctx, next) => {
const { user, post } = ctx.state;
if (post.user._id.toString() !== user._id) {
ctx.status = 403;
return;
}
return next();
};
- MongoDB에서 조회한 데이터의 id값을 문자열과 비교할 때는 반드시 .toString()을 해 주어야 한다.
- 이어서 이 미들웨어를 수정 및 삭제 API에 적용한다.
- checkLoggedIn 미들웨어로 등록해주어야 한다.
[src/api/posts/index.js]
import Router from 'koa-router';
import * as postsCtrl from './post.ctrl';
import checkLoggedIn from '../../lib/checkLoggedIn';
const posts = new Router();
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
const post = new Router(); // /api/posts/:id
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.update);
posts.use('/:id', postsCtrl.getPostById, post.routes());
export default posts;
새로운 계정을 만든 후 그 계정을 사용하여 다른 계정으로 작성된 포스트를 삭제해본다.
[결과]
6. username/tags로 포스트 필터링하기
특정 사용자가 작성한 포스트만 조회하거나 특정 태그가 있는 포스트만 조회하는 기능을 만들어 보겠다.
먼저 조금전에 만든 계정으로 포스트를 작성한다.
포스트 목록 조회 API를 다음과 같이 수정한다.
[src/api/posts/posts.ctrl.js] - list
/*
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;
}
const { tag, username } = ctx.query;
// tag, username 값이 유효하면 객체 안에 넣고, 그렇지 않으면 넣지 않음
const query = {
...(username ? { 'user.username': username } : {}),
...(tag ? { tags: tag } : {}),
};
try {
const posts = await Post.find()
.sort({ _id: -1 })
.limit(10)
.skip((page - 1) * 10)
.exec();
const postCount = await Post.countDocuments(query).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);
}
};
const query = {
...(username ? { 'user.username': username } : {}),
...(tag ? { tags: tag } : {}),
};
- 이 코드는 username 혹은 tag 값이 유효할 때만 객체 안에 값을 넣겠다는 것을 의미한다.
- 다음과 같은 형식으로 query객체를 만들면 어떨까?
{
username,
tags: tag
}
이런 객체를 query로 사용한다면 요청을 받을 때 username이나 tag 값이 주어지지 않는다.
이 경우에는 undefined 값이 들어가게 된다.
mongoose는 특정 필드가 undefined인 데이터를 찾게 되고, 결국 데이터를 조회할 수 없다.
'Front > React' 카테고리의 다른 글
[React] memo의 효과_컴포넌트를 어떻게 분리하는게 리렌더링이 효율적으로 될까? (0) | 2021.05.15 |
---|---|
[React] hooks에서 최적화 (0) | 2021.05.07 |
[React] mongoose를 이용한 MongoDB 연동 실습2 (0) | 2021.01.29 |
[React] mongoose를 이용한 MongoDB 연동 실습1 (0) | 2021.01.26 |
[React] 백엔드 프로그래밍: Node.js의 Koa 프레임워크 (0) | 2021.01.25 |