yup으로 유효성 검증하기

yup으로 매번 작성하는 유효성 코드 지워버리기

Posted by yoogomja on May 3, 2023

클라이언트 개발에 정말 필수적인 요소가 유효성 검증이다. 매번 regex로 복잡한 유효성 코드를 짜두었지만, 결국에는 에러 분기를 나누는 과정에서 코드가 적잖이 지저분해지고는 했었다. 매번 비슷한 유효성 체크 함수를 만드는 것도 좋지 않고 말이다.

그래서 유효성 체크 라이브러리를 찾게 되었는데, 나의 요구 사항은 다음과 같았다.

  • input 마다 유효성 검사를 할 수 있을 것
  • 각 오류마다 아주 간편하게 적절한 메시지를 출력할 수 있을 것

그래서 nestjs에서 활용했던 class-validator와 비슷한 유효성 검사 객체를 찾아보고 있었는데, 그 중에 알게된 것이 yup이다. [yup npm]

How to Use

Installation

1
2
3
yarn add yup

Example : 이메일 검증하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { string, ValidationError } from "yup";

const emailSchema = string()
  .required("이메일이 필요해요 🥲")
  .email("올바른 이메일 형식이 아니에요😅");

const mail = "abc@mail.com";
const wrongMail = "abc";

const main = async () => {
  try {
    const result1 = await emailSchema.validate(mail);
    console.log(result1);
    const result2 = await emailSchema.validate(wrongMail);
    console.log(result2);
  } catch (e) {
    if (e instanceof ValidationError) {
      console.log(e.message);
    }
  }
};

main();

위 내용을 실행하면 다음과 같은 출력이 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ ts-node src/index.ts

abc@mail.com
ValidationError: 올바른 이메일 형식이 아니에요😅
    at createError (/___HIDE___/yup-example/node_modules/yup/index.js:305:21)
    at handleResult (/___HIDE___/yup-example/node_modules/yup/index.js:322:104) {
  value: 'abc',
  path: '',
  type: 'email',
  errors: [ '올바른 이메일 형식이 아니에요😅' ],
  params: {
    value: 'abc',
    originalValue: 'abc',
    label: undefined,
    path: '',
    spec: {
      strip: false,
      strict: false,
      abortEarly: true,
      recursive: true,
      nullable: false,
      optional: false,
      coerce: true
    },
    regex: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
  },
  inner: []
}

✨  Done in 0.71s.

코드에서 볼 수 있듯이, 코드를 검증하는 방식은 다음과 같다.

  1. 검증 조건을 모아둔 Schema를 작성한다.
  2. validate 함수 혹은 isValid 함수에 검사할 변수를 넘겨준다.
  3. 유효성을 검증한다.

validate 함수는 기본적으로 Promise 함수이다. Promise 형식을 사용하지 않으려면 validateSync 활용 할 수 있다. validate의 반환으로는 파라미터에 넘겨준 값을 그대로 내보내준다. 만약, 검증 조건을 벗어나는 경우 에러를 발생 시키게 된다.

이 경우에 오류는 ValidationError라는 형식을 띄기 때문에, 해당 내용만 사용하려면 instanceof 키워드로 걸러줄 필요가 있다. 이렇게 사용하면, 검증과 동시에 연관된 메시지까지 얻을 수 있기 때문에 바로 alert등으로 출력하는 식으로 활용할 수 있다.

검증을 위해서 사용하는 isValid라는 함수도 있는데, 이 함수는 boolean으로 유효성 여부만 돌려주고 오류 메시지는 출력하지 않는다.

Example : Payload 전체를 검증하기

각각 input이 아니라 서버에 전달하려는 payload 전체를 검증해야하는 경우도 있다. 그런 경우에는 object를 활용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { string, object, date, number, ValidationError } from "yup";

const SignUpPayload = object({
  email: string()
    .required("이메일은 필수에요!😱")
    .email("올바른 이메일 형식이 아니에요🧐"),
  birth: date(),
  age: number().min(0).max(150),
});

const payload = {
  email: "test@mail.com",
  birth: "1990-01-31123",
  age: 12,
};

const main = async () => {
  try {
    const validated = await SignUpPayload.validate(payload);
    console.log(validated);
  } catch (error: any) {
    if (error instanceof ValidationError) {
      console.log(error);
      return;
    }
  }
};

main();

object 함수를 사용하게 되면, 객체 형태의 검증 가능하다. 코드를 그대로 실행하면 다음과 같은 오류가 발생하게 되면 다음과 같은 내용이 출력된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ ts-node src/index.ts
ValidationError: birth must be a `date` type, but the final value was: `Invalid Date` (cast from the value `"1990-01-31123"`).
    at Object.createError (___HIDE___/yup-example/node_modules/yup/index.js:305:21)
    at Object.test (___HIDE___/yup-example/node_modules/yup/index.js:944:57)
    at validate (___HIDE___/yup-example/node_modules/yup/index.js:330:44)
    at DateSchema.runTests (___HIDE___/yup-example/node_modules/yup/index.js:708:7)
    at DateSchema._validate (___HIDE___/yup-example/node_modules/yup/index.js:652:10)
    at ___HIDE___/yup-example/node_modules/yup/index.js:747:58
    at ObjectSchema.runTests (___HIDE___/yup-example/node_modules/yup/index.js:708:7)
    at ___HIDE___/yup-example/node_modules/yup/index.js:1744:12
    at nextOnce (___HIDE___/yup-example/node_modules/yup/index.js:694:7)
    at ObjectSchema.runTests (___HIDE___/yup-example/node_modules/yup/index.js:698:24) {
  value: { age: 12, birth: Invalid Date, email: 'test@mail.com' },
  path: 'birth',
  type: 'typeError',
  errors: [
    'birth must be a `date` type, but the final value was: `Invalid Date` (cast from the value `"1990-01-31123"`).'
  ],
  params: {
    value: Invalid Date,
    originalValue: '1990-01-31123',
    label: undefined,
    path: 'birth',
    spec: {
      strip: false,
      strict: false,
      abortEarly: true,
      recursive: true,
      nullable: false,
      optional: true,
      coerce: true
    },
    type: 'date'
  },
  inner: []
}
✨  Done in 0.55s.

오류메시지로 부터 알 수 있는 점은 date, number등의 몇몇 함수들은 자동으로 타입 캐스트를 도와준다는 점이다. 스키마에 작성된 타입과 다르게 일반 문자열을 넣어주면 자동으로 캐스팅해주는데, 실제 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { string, object, date, number, ValidationError } from "yup";

const SignUpPayload = object({
  email: string()
    .required("이메일은 필수에요!😱")
    .email("올바른 이메일 형식이 아니에요🧐"),
  birth: date(),
  age: number().min(0).max(150),
});

const payload = {
  email: "test@mail.com",
  birth: "1990-01-31",
  age: "12",
};

const main = async () => {
  try {
    const validated = await SignUpPayload.validate(payload);
    console.log(validated);
  } catch (error: any) {
    if (error instanceof ValidationError) {
      console.log(error);
      return;
    }
  }
};

main();
1
2
3
4
5
$ ts-node src/index.ts
{ age: 12, birth: 1990-01-30T15:00:00.000Z, email: 'test@mail.com' }
✨  Done in 0.69s.

결과를 보면, 포맷대로 입력한 문자열들을 자동으로 타입 캐스트 해준 것을 알 수 있다. 이렇게 활용하면 input에서 문자열로 입력받고, validate함수의 반환값을 그대로 payload에 넣어주는 식으로 조금 더 엄격한 값 전달을 수행할 수 있다.

마치며

사용하다 보니 미리 schema를 선언해두고 input을 위한 hook을 작성하여도 좋겠다고 생각했는데, 이미 yup을 활용할 수 있게 작성된 패키지가 있다는 것을 알게되었다. react-hook-form이라는 이름의 패키지 인데, 다음에는 이 패키지를 사용해봐야겠다.