본문 바로가기

개발일지

JOI 를 활용한 유효성 체크 기록 (middleware로 확장성있게 활용하자)

목표

회원가입 시 아이디, 패스워드, 닉네임 등을 입력할 때 입력된 값을 유효성을 체크하는 기능을 구현

이전에는..

아래와 같이 각각 체크해야 하는 요소마다 함수를 구현해서 체크했었다.

이렇게 구현하면 router 내 체크해야 할 요소가 많아질수록 코드가 복잡해진다.

//router_register_sample.js 이전에는 이렇게 구현했었다.
const valCheckId = function (target_nickname) {
  const regex_nick = /([a-z]|[A-Z]|[0-9]){3,}/g;
  const nicknameCheckResult = target_nickname.match(regex_nick);

  return nicknameCheckResult == target_nickname;
};

router.post('/', Validator('register'), async (req, res) => {
  try {
    let { userId, userPw, nickname } = req.body;
    let { userId, userPw, nickname } = await userSchema.validateAsync(req.body);

     if (!valCheckId(nickname)) {
       res.status(400).send({
         errorMessage:
           '닉네임은 알파벳 대소문자와 숫자만 사용할 수 있으며, 최소 3자리 이상이어야 합니다.',
       });
       return;
     }

이번 포스팅에 기록할 내용

Joi를 활용하여 좀 더 효율적이고 확장성이 있게 구현해본 내용을 기록하고자 한다.

여러 레퍼런스들을 참고해서 이해하며 적용해보았다. 앞으로 계속 활용하면서  더 깊게 이해하고자 한다.


- Setting

package

npm i joi

directory & files

middlewares

   Validator.js

routers

   router_register.js

validators

   index.js

   register.validator.js


- 코드 및 간략한 실행 흐름

routers/router_register

const { Users } = require('../models');
const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const Validator = require('../middlewares/Validator');

//--- 1. 라우터 호출 시 옵션으로 미들웨어 Validator(argument)를 호출한다. ---
router.post('/', Validator('register'), async (req, res) => {
  try {
    let { userId, userPw, nickname } = req.body;
    
    const salt = crypto.randomBytes(128).toString('base64');
    userPw = crypto
      .createHash('sha512')
      .update(userPw + salt)
      .digest('hex');

    await Users.create({
      userId: userId,
      userPw: userPw,
      nickname: nickname,
      salt: salt,
    });

    res.status(200).send({ msg: '회원가입 완료!' });

  } catch (error) {
    console.log(error);
    res.status(400).send({
      errorMessage: '알 수 없는 오류가 발생했습니다. 관리자에게 문의해주세요.',
    });
  }
});

module.exports = router;

middlewares/Validator.js

const Joi = require("joi");
const Validators = require('../validators')

//--- 2. validators 경로 내 index 파일을 불러와 파라미터로 받은 항목이 있는지 체크한다 ---
module.exports = function(validator) {
  //parameter로 받은 값이 validators/index.js 내 존재하지 않으면 throw error
  if(!Validators.hasOwnProperty(validator))
      throw new Error(`'${validator}' validator is not exist`)

//--- 3. 해당하는 파일이 있다면 그 파일 내 구현한 Schema로 체크한다 ---
  return async function(req, res, next) {
      try {
          const validated = await Validators[validator].validateAsync(req.body)
          req.body = validated
          next()
      } catch (err) {
          if(err.isJoi) 
                res.status(401).send({
                  errorMessage: '입력정보를 다시 확인해주세요.',
                });
      }
  }
}

validators/index.js

//--- 2번에서 본 위치에 export 된 요소가 있는지 체크한다. ---
const register = require('./register.validator')

module.exports = {
  register,
}

//--- validator 기준이 늘어남에 따라 파일이 많아지면 아래와 같이 객체에 추가한다 ---

//const register = require('./register.validator')
//const post = require('./post.validator')

//module.exports = {
//  register, post
//}

validators/register.validator.js

//--- 다양한 조건들을 설정할 수 있다. joi.dev 참고 ---
//--- 3번에서 본 위치의 Schema를 기준으로 체크한다 ---
const Joi = require('joi');

const registerSchema = Joi.object({
  userId: Joi.string().required(),
  userPw: Joi.string().required(),
  nickname: Joi.string().required(),
});

module.exports = registerSchema;

위처럼 미들웨어로 joi를 구현하지 않고,

router 내 joi를 바로 적용한다면 아래와 같이 구현할 수 있다.

const { Users } = require('../models');
const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const Joi = require('joi');

//--- Schema 선언 ---
const userSchema = Joi.object({
  userId: Joi.string().required(),
  userPw: Joi.string().required(),
  nickname: Joi.string().required(),
});

//회원가입
router.post('/', async (req, res) => {

  try {
  //--- Schema 적용 ---
    let { userId, userPw, nickname } = await userSchema.validateAsync(req.body);

    const salt = crypto.randomBytes(128).toString('base64');
    userPw = crypto
      .createHash('sha512')
      .update(userPw + salt)
      .digest('hex');

    await Users.create({
      userId: userId,
      userPw: userPw,
      nickname: nickname,
      salt: salt,
    });

    res.status(200).send({ msg: '회원가입 완료!' });

  } catch (error) {
    console.log(error);
    res.status(400).send({
      errorMessage: '알 수 없는 오류가 발생했습니다. 관리자에게 문의해주세요.',
    });
  }
});

 

만약 Schema 조건에 부합하지 않는 경우, 아래와 같이 콘솔 상 에러를 던져준다.

[Error [ValidationError]: "nickname" must be a string] {
  _original: { userId: 'test009', userPw: 'testpw007', nickname: 23123 },
  details: [
    {
      message: '"nickname" must be a string',
      path: [Array],
      type: 'string.base',
      context: [Object]
    }
  ]
}

이번에 joi를 활용해보면서..

- 미들웨어를 활용하여 확장성 있는 코드를 구축하는 것에 대해 더 깊이 고민해볼 수 있었다.

- joi의 검증 조건에 따라 출력되는 케이스별 에러 메시지를 기준으로 직접 error 메시지를 생성해서 던져주는 방법에 대한 공부가 필요하다.


References

Middleware Based Joi Validation in ExpressJS

https://dev.to/tayfunakgc/middleware-based-joi-validation-in-expressjs-2po5

joi.dev

https://joi.dev/api/?v=17.4.2

Node.js + Express API - Request Schema Validation with Joi

https://jasonwatmore.com/post/2020/07/22/nodejs-express-api-request-schema-validation-with-joi