React 작업의 순서(4): 인증 구현

리린·2021년 8월 21일
1

React

목록 보기
15/47

jwt 구현 : 몽고db 편

  • JWT란?
    데이터가 JSON으로 이루어져 있는 토큰을 의미
    두 개체가 서로 안전하게 정보를 주고받을 수 있도록 웹 표준으로 정의된 기술
  1. User 스키마/모델 만들기
    1) 단방향 해싱 함수 모듈 설치
yarn add bcrypt 

2) 스키마 정의하기
3) 컬렉션 이름과 2)에서 정의한 스키마를 mongoose에 연동시키기.
(이때 컬렌션 이름은 복수형이 된다)
(src/models/user.js)

import mongoose, { Schema } from 'mongoose';

const UserSchema = new Schema({
  username: String,
  hashedPassword: String,
});

const User = mongoose.model('User', UserSchema);
export default User;
  1. 모델 메서드 만들기 : 인스턴스 메서드
  • 인스턴스 메서드: 모델을 통해 만든 문서 인스턴스에서 사용 가능한 메서드
  • 여기서 this는 인스턴스를 의미한다. (화살표 함수 사용 금지)
  • 예시
    (App.js)
const user = new User({username: 'velopert'});
user.setPassword('mypass123');

1) 비밀번호 세팅 메서드 만들기

(src/models/user.js)

import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';

(..) 
UserSchema.methods.setPassword = async function(password) {
  const hash = await bcrypt.hash(password, 10);
  this.hashedPassword = hash;
};

2) 비밀번호 체크 메서드 만들기

UserSchema.methods.checkPassword = async function(password) {
  const result = await bcrypt.compare(password, this.hashedPassword);
  return result; // true / false
};
  1. 모델 메서드 만들기: 스태틱 메서드
  • 스태틱 메서드: 모델에서 바로 사용할 수 있는 메서드
  • 여기서 this는 Model(=User)을 가리킨다.
  • 예시
const user = User.findByUsername('velopert');

1) 유저이름 찾기

UserSchema.statics.findByUsername = function(username) {
  return this.findOne({ username });
};

jwt 구현 : 회원 인증 API 틀 만들기

  1. 컨트롤러 틀 정의
    (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 => {
  // 로그아웃
};
  1. 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;
  1. 2를 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;

jwt 구현 : 회원 가입 구현하기

  1. 컨트롤러 파일부터 작성
    (src/api/auth/auth.ctrl.js)
    1) 필요한 모듈 가져오기
import Joi from 'joi' 
import User from '../../models/user';

2) register 컨트롤러 함수 만들기

  • Joi로 username 과 password 검증하는 스키마 만들기
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(),
  });
  .
  .
  • 결과에 ctx.request.body를 스키마에 넣고 검증한 데이터를 넣기
const result = schema.validate(ctx.request.body);
  • 만약 에러가 생기면 ctx.status에 400을 넣고, ctx.body에 result.error을 넣는 뒤 return한다.
if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }
  • ctx.request.body(요청객체)에서 username, password를 가져온다
 const { username, password } = ctx.request.body;
  • username이 존재하는지 확인한다
const exists = await User.findByUsername(username);
  • 만약 존재하는 경우: ctx.status에 409를 넣고, 리턴한다.
if (exists) {
      ctx.status = 409; // Conflict
      return;
    }
  • 모델의 인스턴스 문서를 우선 'username'만 갖고 생성한다
 const user = new User({
    username,
    });
  • 비밀번호는 비동기로, setPassword(passsword)를 가지고 설정한다.
 await user.setPassword(password); 
  • 데이터베이스에 저장한다.
await user.save(); 
  • 응답 데이터에서는 hashedPassword 필드를 제거한다.
const data = user.toJSON();
delete data.hashedPassword();
ctx.body=data;
  • 에러를 잡는다.
catch (e) {
    ctx.throw(500, e);
  }
  • 전체파일
    (src/api/auth/auth.ctrl)- 회원가입
import Joi from '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;
    return;
  }

  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);
  }
};
  • 팁: 리팩터링
    (메서드로 리팩터링하기: hashedPassword필드가 응답되지 않도록 데이터를 JSON으로 변환한 다음 delete를 통해 해당 필드를 지워주는 것)
    (src/models/user.js-serialize 메서드 추가)
UserSchema.methods.serialize = function() {
  const data = this.toJSON();
  delete data.hashedPassword;
  return data;
};

(src/api/auth/auth.ctrl.js 기존 코드 대체)

export const register = async ctx => {
    (...)

    const user = new User({
      username,
    });
    await user.setPassword(password); // 비밀번호 설정
    await user.save(); // 데이터베이스에 저장

    ctx.body = user.serialize();
  } catch (e) {
    ctx.throw(500, e);
    }
};

jwt 구현 : 로그인 구현하기

  1. 컨트롤러 함수 작성: 로그인 구현하기

0) 비동기는 필수
1) ctx.request.body에서 username 과 password 빼오기

export const login = async ctx => {
  const { username, password } = ctx.request.body;

2) 만약 1에서 에러가 발생하면( = username 이나 password 가 없으면) ctx.status에 401 세팅하고 return 하기

if (!username || !password) {
    ctx.status = 401; // Unauthorized
    return;
  }

3) 스테틱 메서드인 .findByUsername(username)으로 username 찾아 user 에 저장하기

try {
    const user = await User.findByUsername(username);

4) 만약 3에서 에러가 발생하면(= username 이 없으면) ctx.status에 401 세팅하고 return 하기

if (!user) {
      ctx.status = 401;
      return;
    }

5) 스테틱 메서드인 .checkPassword(password)로 password 찾아 valid 에 저장하기

const valid = await user.checkPassword(password);

6) 만약 5에서 에러가 발생하면(=비밀번호가 잘못되었으면) ctx.status에 401 세팅하고 return 하기

if (!valid) {
      ctx.status = 401;
      return;
    }

7) 인스턴스 메서드인 user.serialize() (해쉬 비밀번호 제외하기) 로 해쉬 비밀번호 제외하고 ctx.body 에 user값 실어 보내기

ctx.body = user.serialize();

8) 에러 나면 ctx.status에 500 세팅하고 return 하기

catch (e) {
    ctx.throw(500, e);
  }

(전체파일: 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);
    // 잘못된 비밀번호
    if (!valid) {
      ctx.status = 401;
      return;
    }
    ctx.body = user.serialize();
  } catch (e) {
    ctx.throw(500, e);
  }
};

jwt 구현 : 토큰 발급 및 검증하기

  1. 모듈 설치하기
yarn add jsonwebtoken 
  1. 비밀키 설정하기
    (.env)
PORT=4000
MONGO_URI=mongodb://localhost:27017/blog
JWT_SECRET=33e86ac48e993d06bb3afd382863457e6bca79daf983ec4ba783e75999b1f4a000e686223b87f05659f7f85e0d369a596d32d2df6f76c41fca708c23a7c15488
  1. 토큰 발급하기
    1) jwt 모듈 가져오기
import jwt from 'jsonwebtoken';

2) 인스턴스 메서드 .generateToken 추가하기
3) token 상수 첫 번째 인자에 _id와 username이 키로 들어간 객체를 넣는다.

UserSchema.methods.generateToken = function() {
  const token = jwt.sign(
    // 첫 번째 파라미터에는 토큰 안에 집어넣고 싶은 데이터를 넣습니다.
    {
    _id: this.id,
      username: this.username,
    },

4) token 상수 두 번째 인자에 process.env.JWT_SECRET 키를 넣는다.

process.env.JWT_SECRET,

5) token 상수 세 번째 인자에 유효기간을 넣는다.

{
      expiresIn: '7d', // 7일 동안 유효함
    },

6) 마지막으로 token을 반환한다. ( 이 메서드는 토큰을 발행하는 메서드 )

return token;
};

(전체파일)

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;
};
  1. 토큰을 쿠키에 담아 사용한다.
    (src/api/auth/auth.ctrl.js -register, login )
    1) register 함수 해쉬 제거 메서드 바로 밑에 user.generateToken()을 사용하여 토큰을 생성한다.
export const register = async ctx => {
  (...)
    ctx.body = user.serialize();

    const token = user.generateToken();

2) 쿠키를 세팅한다. 이때 첫 번째 인자는 키로 'access_token'이고, 두 번째 인자는 value로 token을 넣는다. 세 번째 인자는 유효기간을 표시하는 maxAge와 js로 토큰 조회가 불가능하게 하는 httpOnly속성을 넣는다.

ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
      httpOnly: true,
    });

3) 여기서 에러가 나면 500을 던진다.

} catch (e) {
    ctx.throw(500, e);
  }
 

4) 동일한 작업을 반복한다.

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);
  }
};
  1. 토큰 검증하는 미들웨어 추가하기
    (src/lib/jwtMiddleware.js)
    1) jwt 모듈을 가져온다
import jwt from 'jsonwebtoken';

2) 쿠키에서 'access_token'키로 토큰을 가져온다.

const token = ctx.cookies.get('access_token');

3) 2)에서 에러가 난다(=토큰이 없다) 면 next()를 리턴한다.

if (!token) return next(); // 토큰이 없음

4) jwt.verify의 첫 번째 인자는 토큰, 두 번째 인자는 JWT_SECRET 키를 사용하여 decoded 값을 구한 뒤, next()를 리턴한다

try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    console.log(decoded);
    return next();

5) 4)에서 에러 발생시 리턴한다

catch (e) {
    // 토큰 검증 실패
    return next();
  }

(전체 파일: 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;

6) env 파일을 모듈로 사용한다

require('dotenv').config();

7) jwt 미들웨어를 가져온다

import jwtMiddleware from './lib/jwtMiddleware';
(...)

8) 미들웨어를 적용한다

app.use(jwtMiddleware);

9) app 인스턴스에 라우터를 적용한다

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

(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());
(...)
  1. 토큰 검증하기
    (src/api/auth/auth.ctrl.js-check)
    1) ctx.state에서 user 빼내오기
export const check = async ctx => {
  const { user } = ctx.state;

2) 1) 에서 에러면 ctx.status에 401 세팅하고 return

if (!user) {
    // 로그인 중 아님
    ctx.status = 401; // Unauthorized
    return;
  }

3) ctx.body에 user값 세팅하기

ctx.body = user;
};

4) 전체 파일
(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;
};
  1. 토큰 재발급하기
    (src/lib/jwtMiddleware.js) 다시 가져오기
    1) 현재 시간 초단위로 구하기
   const now = Math.floor(Date.now() / 1000);

2) token 해제한 것의 exp(유효기간) 에서 현재를 뺀 것이 3.5일 미만일 경우

    if (decoded.exp - now < 60 * 60 * 24 * 3.5)

3) .findById의 인자를 해체된 토큰의 _id로 하여 찾아 user에 넣는다.

const user = await User.findById(decoded._id);

4) 이 유저값으로 다시 토큰을 생성한다

      const token = user.generateToken();

5) 쿠키에 생성한 토큰을 다시 세팅한다.

ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 7, // 7일
        httpOnly: true,
      });

6) next()를 리턴한다

}
    return next();

7) 실패할 경우 에러처리를 한다.

catch (e) {
    // 토큰 검증 실패
    return next();
  }

8) 전체 파일
(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;

jwt 구현 : 로그아웃 기능

  1. 컨트롤러 함수 추가하기
    1) 쿠키를 제거한다
    2) status를 204로 세팅한다
/*
  POST /api/auth/logout
*/
export const logout = async ctx => {
  ctx.cookies.set('access_token');
  ctx.status = 204; // No Content
};
  

posts API 에 회원 인증 시스템 도입하기

  1. post 모델에 id 와 username 넣기
    (src/models/post.js)
import mongoose, { Schema } from '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;
  1. 로그인할 때만 api를 사용할 수 있도록 하는 미들웨어 만들기
    1) ctx.state.user 가 존재하면 다음으로 넘기지만
    2) 없으면 ctx.status 에 401을 세팅하고 리턴하기
    (src/lib/checkLoggedIn.js)
const checkLoggedIn = (ctx, next)=>{
	if(!ctx.state.user){
    ctx.status=401;
    return; 
    }
    return next();
 }
  1. 2를 post라우터에서 실행하기
    1) checkLoggedIn 을 가져오기
    2)
    (src/api/posts/index.js)
import Router from 'koa-router';
import * as postsCtrl from './posts.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;
  1. 포스트 작성 시 사용자 정보 넣기
    (src/api/posts/posts.ctrl.js -write)
    1) title, body, tags를 ctx.request.body에서 가져오기
 const { title, body, tags } = ctx.request.body;

2) Post 모델의 post 인스턴스 문서(아이템 하나)를 title과 body, tags, user 값을 사용하여 만들기

const post = new Post({
    title,
    body,
    tags,
    user: ctx.state.user,
  });

3) post.save()로 Post 모델에 저장하기

await post.save();

4) ctx.body에 post를 담아 보내기

ctx.body = post;

5) 4에서 에러가 나면 500던지기

catch (e) {
    ctx.throw(500, e);
  }

6) 전체 파일
(src/api/posts/posts.ctrl.js -write)

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);
  }
};
  1. 포스트 수정/삭제 시 권한 확인하기
  • ctx.params의 아이디로 post를 찾아내서 가져오기(기존에는 checkObjectId)
  • 수정 및 삭제 api의 미들웨어다.
    (src/api/posts/posts.ctrl.js -getPostById)
  • 여기서 id는 유저의 id다.
    1) ctx.params에서 id 가져오기
export const getPostById = async (ctx, next) => {
  const { id } = ctx.params;

2) 만약 id가 올바르지 않다면 ctx.status에 400 입력하고 return 하기

if (!ObjectId.isValid(id)) {
    ctx.status = 400; // Bad Request
    return;
  }

3) 스태틱 메서드인 .findById(id)로 Post에서 post 아이템(=인스턴스 문서) 차지

  • 왜냐면, id는 이제 post의 id가 되었으니까.
const post = await Post.findById(id);

4) 3에서 post 가 존재하지 않으면 ctx.status에 404 넣고 return 하기

if (!post) {
      ctx.status = 404; // Not Found
      return;
    }

5) ctx.state.post에 post 넣어주고 다음 컨트롤러로 넘기기

ctx.state.post = post;
    return next();

6) 3~5에서 에러가 나면 500 던지기

catch (e) {
    ctx.throw(500, e);
  }
};

7) 전체파일
(src/api/posts/posts.ctrl.js -getPostById)

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.status = 404; // Not Found
      return;
    }
    ctx.state.post = post;
    return next();
  } catch (e) {
    ctx.throw(500, e);
  }
};

8) 라우터에 반영하기
(src/api/posts/index.js)

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

export default posts;
  
  1. read 함수 간소하게 만들기
    (src/api/posts/posts.ctrl.js)
export const read = ctx => {
  ctx.body = ctx.state.post;
};
  1. checkOwnPost 미들웨어 만들기
  • id로 찾은 포스트가 로그인 중인 사용자가 작성한 포스트인지 확인해줌
    (src/api/posts/posts.ctrl.js)
    1) ctx.state에서 user와 post 가져오기
export const checkOwnPost = (ctx, next) => {
  const { user, post } = ctx.state;

2) post의 user정보의 id를 toString()으로 문자열화한 것과 user정보의 _id가 같지 않으면 ctx.status에 403을 세팅하고 return하기

if (post.user._id.toString() != = user._id) {
    ctx.status = 403;
    return;
  }

3) next()를 리턴하기

return next();
};

4) 전체 파일
(src/api/posts/posts.ctrl.js)

export const checkOwnPost = (ctx, next) => {
  const { user, post } = ctx.state;
  if (post.user._id.toString() != = user._id) {
    ctx.status = 403;
    return;
  }
  return next();
};

5) 수정 및 삭제 API 에 적용하기

  • checkLoggedIn 다음 미들웨어로 등록해야 함
    (src/api/posts/index.js)
import Router from 'koa-router';
import * as postsCtrl from './posts.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;

usename/tags로 포스트 필터링하기

  • username 혹은 tag 둘 중 하나라도 쿼리에 담으면 이를 통해 포스트를 찾아준다.
  • 거기에 페이지 기능과 최대 200자 기능까지 구현했다.
  1. 컨트롤러
    1) page 값은 ctx.query.page에 있는 값이다. 없을 경우 기본은 1페이지다
export const list = async ctx => {
  // query는 문자열이기 때문에 숫자로 변환해 주어야 합니다.
  // 값이 주어지지 않았다면 1을 기본으로 사용합니다.
  const page = parseInt(ctx.query.page || '1', 10);

2) page가 1이 안 될 경우 ctx.status에 400을 넣고 return 한다

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

3) 이번에는 ctx.query에서 tag과 username을 꺼낸다

const { tag, username } = ctx.query;

4) username, 혹은 tag가 존재할 경우에만 각각 값을 넣어주어 찾는다.

const query = {
    ...(username ? { 'user.username': username } : {}),
    ...(tag ? { tags: tag } : {}),
  };

5) 쿼리로 찾은 포스트들에 각종 제약을 걸어두어 정제한다 (역순정렬, 10개씩 보여주기, page에 따라 스킵해서 보여주기(예시: 20번째~29번째), json으로 변경 등 )


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

6) 스태틱 메서드인 Post.countDocuments(query)로 개수를 구한다

const postCount = await Post.countDocuments(query).exec();

7) 마지막 페이지는 postCount에 10을 나눠서 구한다

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

8) ctx.body에는 Posts들이 들어가되, 본문을 200자 내외로 제외한 배열이 들어간다

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

9) 5~ 8사이의 에러는 잡아낸다

catch (e) {
    ctx.throw(500, e);
  }

10) 전체 파일

/*
  GET /api/posts?username=&tag=&page=
*/
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(query)
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .lean()
      .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);
  }
};

프로젝트 마무리

창 띄우기

포스팅 구현하기

profile
개발자지망생

0개의 댓글