시작하기

API 수준이든, 함수 수준이든 외부 입력을 항상 validation하는 습관은 중요하다. 귀찮다고 빼 먹는다면 언젠가는 분명 문제를 일으킬 것이다. 많은 node용 validator가 있지만 Joi가 우수한 점은 관계에 따른 내용을 정의할 수 있다는 점이다. 예를 들어 ab가 둘 중에 하나라도 있으면 된다든가, a가 있을 때만 b가 있어야 된다는 식이다.

여기서 다루는 Joi의 모든 기능은 API 13.6.0을 따르고 있다.

관계에 따른 Joi 사용법은 뒤에서 알아보고 우선은 Joi의 간단한 사용법부터 알아보자.

const Joi = require('joi');

// validate할 schema 정의
const schema = Joi.object().keys({
  username: Joi.string().min(3).max(30).required(),
  birthyear: Joi.number().integer().min(1900).max(2018),
});

// validate할 object
const user = {
  username: 'username1',
};

// validate
const {error, value} = Joi.validate(user, schema);
console.log(error); // null
console.log(value); // {username: "username1"}

입력인 user가 미리 정의된 schema와 일치하는지를 확인한다.

schema는 다음과 같이 정의되었다.

  • username: string이고, 길이가 최소 3글자에서 최대 30글자이며, 필수 항목임.
  • birthyear: number이고, integer이며, 최소 1900에서 최대 2013까지 허용하고, 옵션 항목임.

따라서 user.usernameschema에 정의된 내용에 부합하고, user.birthyear는 optional로 정의되었기 때문에 없어도 문제 없이 validate되었다.

에러가 나는 경우는 어떻게 되는지 알아보자.

const Joi = require('joi');

const schema = Joi.object().keys({
  username: Joi.string().min(3).max(30).required(),
  birthyear: Joi.number().integer().min(1900).max(2018),
});

const user = {
  username: 'username1',
  birthyear: 2020,
};
const {error, value} = Joi.validate(user, schema);
console.log(error); // Error: child "birthyear" fails because ["birthyear" must be less than or equal to 2018]
console.log(value); // {username: "username1", birthyear: 2020}

user.birthyear가 정의된 최대값 2018보다 크기 때문에 validate error가 났다. Joi.validate return 값의 value는 원래 입력값인 user의 내용이 그대로 표시되므로, return 값의 error가 null 인지 아닌지로 validate 성공, 실패 여부를 확인하면 된다.

Joi의 동작

기본적인 중요한 부분은 훑었으니, Joi의 동작 방식을 몇 가지 알아보자.

.unknown() API

schema에 정의되지 않은 key는 validate error가 난다. 이를 피하려면 .unknown() API를 쓰면 된다.

const Joi = require('joi');

// .unknown()이 없는 경우
const schema1 = Joi.object().keys({
  username: Joi.string().min(3).max(30).required(),
});

const user1 = {
  username: 'username1',
  birthyear: 2014,   // schema1에 정의되지 않은(unknown) 'birthyear'
};
const {error: error1, value: value1} = Joi.validate(user1, schema1);
console.log(error1); // Error: "birthyear" is not allowed
console.log(value1); // {username: "username1", birthyear: 2014}


// .unknown()이 있는 경우
const schema2 = Joi.object().keys({
  username: Joi.string().min(3).max(30).required(),
})
  .unknown();  // 정의되지 않은 key도 허용함.

const user2 = {
  username: 'username1',
  birthyear: 2014,   // schema1에 정의되지 않은(unknown) 'birthyear'
};
const {error: error2, value: value2} = Joi.validate(user2, schema2);
console.log(error2); // null
console.log(value2); // {username: "username1", birthyear: 2014}

abortEalry option

정의된 key 중 에러가 나면 더 이상 진행하지 않는 것이 기본 동작(abortEarly: true)이다. 한 번에 모든 에러를 확인하고 싶으면 validate 시점에 동작을 제어할 수 있는 validate()의 세 번째 파라미터{abortEarly: false}를 설정하면 된다.

const Joi = require('joi');

const schema = Joi.object().keys({
  username: Joi.string().min(3).max(30).required(),
  birthyear: Joi.number().integer().min(1900).max(2018),
});

const user = {
  birthyear: 2020,
};

const options: {
  abortEarly: false,
};

const {error, value} = Joi.validate(user, schema, options);
console.log(error); // Error: child "username" fails because ["username" is required]. child "birthyear" fails because ["birthyear" must be less than or equal to 2018]
console.log(value); // {birthyear: 2020}

.required()username이 없는 error와 .max(2018)을 위반한 birthyear가 모두 validate error로 검출되었다.

이 외에도 여러가지 option이나 API가 있으니, 상황에 맞게 사용하면 된다.

관계 API

끝으로, 관계에 관련된 API를 알아보자.

.and, .or, .nand, .xor는 bit 연산과 동일하게 생각하면 된다. 예를 들어 .or는 정의된 key가 하나 이상이나 모두 있으면 되지만, .xor는 정의된 key가 모두 있는 경우에 validate error가 난다.

const Joi = require('joi');

const schema = Joi.object().keys({
  handphone: Joi.string(),
  homephone: Joi.string(),
})
  .xor('handphone', 'homephone')
  ;

const user = {
  handphone: '01011112222',
  homephone: '023334444',
};
const {error, value} = Joi.validate(user, schema);
console.log(error); // Error: "value" contains a conflict between exclusive peers [handphone, homephone]
console.log(value); // {handphone: "01011112222", homephone: "023334444"}

handphonehomephone.and로 묶었기 때문에 homephone이 없어서 validate error가 발생했다.

.with, .without은 기준 key에 대해서 있어야 되거나 없어야 되는 관계를 설정한다.

const Joi = require('joi');

const schema = Joi.object().keys({
  address1: Joi.string(),
  address2: Joi.string(),
})
  .with('address1', 'address2')
  ;

const user = {
  address1: 'address1',
};
const {error, value} = Joi.validate(user, schema);
console.log(error); // Error: "address1" missing required peer "address2"
console.log(value); // {address1: "address1"}

address1가 있는데 address2가 없는 경우에만 validate error가 난다.

끝으로 key 관계에 조건을 걸 수 있는 .when 예제를 보자.

const Joi = require('joi');

const schema = Joi.object().keys({
  handphone: Joi.string(),
  homephone: Joi.string(),
  mobile_os: Joi.string().when('handphone', {
    is: Joi.exist(),
    then: Joi.string().required(),
    otherwise: Joi.forbidden(),
  }),
})
  ;

위 schema를 말로 표현하면 mobile_oshandphone이 있는 경우에만 유효하고, handphone이 없는데 mobile_os가 있다면 validate error가 난다.

// handphone과 mobile_os가 모두 있어 validate success
const user1 = {
  handphone: '01011112222',
  mobile_os: 'ios',
};
const {error: error1, value: value1} = Joi.validate(user1, schema);
console.log(error1); // null
console.log(value1); // {handphone: "01011112222", mobile_os: "ios"}

// handphone이 없는데  mobile_os가 있어 validate error
const user2 = {
  homephone: '023334444',
  mobile_os: 'ios',
};
const {error: error2, value: value2} = Joi.validate(user2, schema);
console.log(error2); // Error: child "mobile_os" fails because ["mobile_os" is not allowed]
console.log(value2); // {homephone: "023334444", mobile_os: "ios"}