이번엔 저번 포스팅에서 하지 못했던 커스텀 validator 를 생성해 컨트롤러가 받는 DTO 의 일부 필드에 적용해보도록 하겠습니다
- Custom Validator
validator 를 커스텀하게 만들려면 class-validator 에서 제공하는 기능을 사용하면 됩니다. 이번 예시에서도 공식 내용을 따라가면서
만들어보도록 하겠습니다.
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsXXX(property: string, validationOptions?: ValidationOptions) {
return (object: object, propertyName: string) => {
registerDecorator({
name: 'IsXXX',
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [property],
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return typeof value === 'string' && typeof relatedValue === 'string' && value.length > relatedValue.length;
},
},
});
};
}
일단 긁어온 예시입니다. 꼭 정해진건 아니지만 대체로 유효성을 체크하는 데코레이터이기 때문에 앞에 prefix 로 Is 를 쓰게 되었습니다.
먼저 볼 것은 class-validator 에서 import 하는 함수 및 인터페이스들입니다.
registerDecorator 는 네이밍도 그렇고 위 코드에서 보이는 것처럼 해당 데코레이터를 등록하는 역할을 합니다.
이 registerDecorator 는 인자로 ValidationDecoratorOptions 타입을 받는데 그 타입에 들어가는 속성들은 코드에 나와있습니다.
ValidationOptions 는 위에서도 옵셔널로 선언한 것 처럼 별로 중요한 역할은 아니지만 해당 validator 가 실패했을 때 나오는
메세지등을 해당 인자에 커스텀하게 집어 넣을 수 있습니다.
ValidationArguments 는 저것만 봐선 뭐가 뭔지 알기는 어렵고 validator, contraints 등을 같이 봐야합니다.
먼저 validator 는 말 그대로 커스텀한 검사 규칙 코드를 갖고 있는 속성이고, 이는 ValidatorContraint interface 를 구현한 함수라서
그 내부에선 validate 함수를 구현해야 합니다.
validate 함수는 boolean 을 리턴하도록 작성하면 되는데 true 면 유효성 체크를 통과한것이고, false 면 유효성 체크를 통과하지 못한 것입니다.
class XXXDto {
readonly a: string;
@IsXXX('a', { message: 'blah blah blah' })
readonly b: string;
}
아무래도 예시를 드는게 편할 듯 해서..
위에서 작성한 IsXXX 데코레이터를 XXXDto 의 b 속성에 유효성 validator 로 사용하기 위해 위와 같이 추가했습니다.
이러면 property 는 'a' 가 되고 validationOptions 엔 message 프로퍼티를 가진 리터럴 객체가 들어가는데 뭐 이건 이미 얘기한것처럼
만약 validator 가 유효성 검사에 실패했을 경우 메세지로 'blah blah blah' 가 뜨는 정도의 역할입니다.
property 로 'a' 가 들어온 상태에서 registerDecorator 를 호출하는 함수를 반환하는 함수의 인자인 object 와 propertyName 은
각각 XXXDto 와 'b' 가 됩니다. 즉 object 는 이 데코레이터가 선언될 객체, propertyName 은 그 속성 이름이 됩니다.
그 밑에 constraints 를 보면 [property] 가 되어있는데 이걸 통해서 우리는 이 데코레이터의 제약조건으로 'a' 를 사용할 것이라고 선언할 수 있습니다. 당연히 이 'a' 는 XXXDto 에 선언되어있는 프로퍼티를 말합니다.
constraints 에 선언했기 때문에 우리는 validator 의 validate 함수에서 'a' 프로퍼티에 접근할 수 있습니다.
코드를 그대로 해석하자면 relatedPropertyName 은 'a' 가 되고, relatedValue 는 유효성 검사를 할 때 a 에 들어온 실제 값이 되며
a 와 b 가 둘 다 string 이고 b 의 길이가 a 보다 길어야 통과한다 라는 validator 가 되는 것입니다.
여기까지 예시 코드 해석이 되었으니.. 저는 제가 만들고 싶은 validator 를 만들도록 하겠습니다.
먼저 저는 src 하위에 decorators/index.ts 와 types/country.ts 파일을 생성했습니다.
export enum CountryCode {
KR = 'KR',
US = 'US',
}
types/country.ts 에는 최대한 간략히 넣어봤습니다.
당장 지원하는 국가는 KR 과 US 로 가정하고 두 국가의 2자리 국가코드만 enum 에 넣습니다.
import { registerDecorator, ValidationOptions } from 'class-validator';
import { CountryCode } from '../types/country';
export function IsAlpha2Country(validationOptions?: ValidationOptions) {
return (object: object, propertyName: string) => {
registerDecorator({
name: 'IsAlpha2Country',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return Object.values(CountryCode).includes(value);
},
},
});
};
}
decorators/index.ts 는 예시에 비하면 비교적 심플한데 저는 광고 생성시 입력받는 국가의 값을 제공하는 enum 이 아닌건 400 에러로 처리하는 validator 를 만들고자 했습니다.
그래서 데코레이터 이름은 IsAlpha2Country 라고 지었고, 다른 프로퍼티와 비교할건 아니라서 인자에서 property 는 제거했습니다.
validator 의 validate 구현부는 value 가 enum 에 속하는지를 보는 정도로 간단합니다.
...
import { IsAlpha2Country } from '../../decorators';
import { CountryCode } from '../../types/country';
export class CreateAdDto {
...
@IsAlpha2Country({
message: '지원하는 국가의 2자리 국가코드를 입력해야 합니다',
})
readonly country: CountryCode;
}
CreateAdDto 의 변경된 부분만 보자면 위와 같습니다. 기존 class-validator 가 제공하는 데코레이터 사용하는 것과 똑같이 사용하면
되는데 validationOptions 에 메세지만 조금 추가를 해봤습니다. enum 체크를 할거라 기존에 있었던 @IsString, @Length(2, 2) 는
불필요하기에 제거했구요. 이제 한번 테스트를 해보겠습니다.
이런식으로 enum 에서 제공하지 않는 국가 코드 또는 KOR 등의 3자리 국가코드를 넣게되면 custom validator 에 의한 유효성 검사가
실패하면서 message 도 제가 넣었던 내용으로 뜨는 것을 볼 수 있습니다. KR 또는 US 로 넣으면 정상적으로 처리되는 것 또한 확인이
가능합니다.
음 근데 이렇게 적고나서 class-validator 를 좀 보다보니 이미 @IsEnum 이라는 데코레이터를 지원하는군요.
...
import { IsEnum } from 'class-validator';
import { CountryCode } from '../../types/country';
export class CreateAdDto {
...
@IsEnum(CountryCode, {
message: '지원하는 국가의 2자리 국가코드를 입력해야 합니다',
})
readonly country: CountryCode;
}
제공하는 @IsEnum 데코레이터를 사용해서도 동일한 처리가 가능합니다.
커스텀 validator 를 작성하기에 앞서 필요한 데코레이터를 제공하는지 먼저 확인하는 것이 중요하겠습니다. (링크)
- Use Validator by class
위에서 validator 는 ValidatorContraint interface 를 구현한 함수라고 했는데, 말 그대로 ValidatorContraintInterface 를 구현하는
클래스를 별도로 만들어서 validator 에 넣는 것 또한 가능합니다. 여기엔 옵션을 추가해서 비동기 작업도 쉽게 넣을수가 있구요.
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
function findAdType(adType: string): Promise<boolean> {
return Promise.resolve(adType !== '1');
}
@ValidatorConstraint({ async: true })
export class IsAdTypeAlreadyExistConstraint
implements ValidatorConstraintInterface
{
validate(adType: any) {
return findAdType(adType).then((isExist) => {
return isExist;
});
}
}
export function IsAdTypeAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsAdTypeAlreadyExistConstraint,
});
};
}
이건 정말 단순 예시로만 봐주시면 좋을 것 같습니다. 그래서 억지로 끼워넣긴했고.. 예시로만 보이고 다시 코드에선 제거할겁니다.
AdType 이 '1' 로 들어오면 유효성 체크를 실패로 처리하기 위한 데코레이터인데, 위에서 얘기한대로 ValidatorConstraintInterface 를 구현하는 클래스를 validator 에 넣었고 validate 는 Promise 를 반환하고 있지만 ValidatorConstraint 에 async 옵션을 줘서 별다른
컨트롤 없이 사용이 가능합니다.
import { IsAdTypeAlreadyExist } from '../../decorators';
export class CreateAdDto {
@IsString()
@IsAdTypeAlreadyExist({ message: 'AdType $value already exists'})
readonly adType: string;
...
}
CreateAdDto 의 adType 프로퍼티에 위와 같이 데코레이터를 추가했습니다. 이번에 보면서 알게된건데 템플릿 리터럴을 쓰듯이 저렇게 표현하는 것도 상당히 편했습니다. 이제 adType 에 '1' 을 넣고 요청을 보내보겠습니다.
의도한대로 '1' 을 넣었을 때 400 에러가 떨어지면서 에러에 대한 정보 메세지도 잘 나오는 것을 확인할 수 있습니다.
npm class-validator 의 예시 코드에선 validate 할 때 repository 까지 접근해서 중복 체크를 하는데 그게 파이프의 역할로 볼 수
있을지는 저는 조금 의문이라 일단 예시만 작성해보고 넘어갑니다.
실제 여러 비즈니스 요구사항을 구현하는 API 서버를 작업하다보면 요청 페이로드에는 다양한 프로퍼티가 들어오게되는데,
class-validator 가 이미 제공하는 많은 데코레이터로도 충분히 커버가 가능하지만 특수한 경우에는 위처럼 별도의 커스텀 validator 를
생성해 사용한다면 코드가 훨씬 관심사별로 깔끔하게 분리도 되면서 비즈니스 로직 구현에 더 집중할 수 있는 환경이 될 수 있어보입니다.
'Backend > NestJS' 카테고리의 다른 글
NestJS - 대충 서비스 만들어보기 (8) (0) | 2023.05.01 |
---|---|
NestJS - 대충 서비스 만들어보기 (7) (0) | 2023.04.23 |
NestJS - 대충 서비스 만들어보기 (5) (0) | 2023.04.08 |
NestJS - 대충 서비스 만들어보기 (4) (0) | 2023.03.25 |
NestJS - 대충 서비스 만들어보기 (3) (0) | 2023.03.19 |