NestJS를 개발하다 보면 들어오는 요청에 대해 유효성검사를 수행해야 할 때가 있습니다. NestJS에서는 class-validator, class-transformer가 구축된 ValidationPipe를 제공해 주는데 이를 통해 유효성 검사를 수행할 수 있습니다.
저는 개발 중에 기본적으로 제공하는 class-validator의 데코레이터가 아닌 커스텀한 Validation 데코레이터를 작성할 필요가 있었는데요, 기본적으로 class-validator가 많은 데코레이터를 제공해 주지만 원하는 데코레이터가 없을 때 어떻게 적용하는게 좋은지 정리해 보도록 하겠습니다.
NestJS의 유효성 검사
먼저 class-validator의 기본 데코레이터를 활용하여 유효성을 검사하는 적용하는 방법을 한번 알아보겠습니다. HTTP 요청에 대해서 Request Body에 대한 유효성을 검사할 때 아래와 같이 유효성을 검증할 수 있습니다.
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class SignUpDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
password: string;
}
위 예제는 회원가입을 처리하는 DTO인데요, @IsNotEmpty, @IsEmail 등의 데코레이터를 통해 원하는 유효성 검사에 대한 내용을 지정하면 유효성 검증에 실패했을 시, 기본적인 에러메세지와 함께 Client로 실패 메세지가 전송되게 됩니다.
만약 요청 시 Validation에서 유효성 검증이 성공했다면, Controller에서 @Body 데코레이터를 통해 값을 받아와서 다음 로직을 처리할 수 있습니다.
@Post()
async signUp(@Body() body: SignUpDto) {
const { email, password } = body; // 컨트롤러에서 값을 전달받음
...
}
원하는 제약조건을 직접 만들어서 유효성 검사 수행하기 (@Validate 데코레이터 사용)
기본적인 데코레이터로도 대부분의 유효성 처리를 쉽게 처리할 수 있지만 실제로 개발을 하다 보면, 기본적으로 제공하는 Validation 데코레이터가 아닌 특수성이 있는 유효성 검증을 수행해야 할 때가 있습니다.
먼저 이해를 위해서 예시를 한번 들어보겠습니다. 제가 만들고 싶은 유효성 검증 시나리오중에, 특정 field의 값이 n차원 배열이면서, 해당 배열의 값이 모두 number타입인 배열만 입력을 받을 수 있는 유효성 검증이 필요하였습니다. 하지만 위와 같은 유효성 검증을 하는 데코레이터는 class-validator에서 기본적으로 제공하지 않습니다.
이런 상황에서는 아래와 코드의 예제처럼 원하는 유효성에 대한 제약조건을 직접 구성한 뒤 @Validate 데코레이터를 통해 적용시키면 됩니다.
// is-number-array.decorator.ts
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
Validate,
} from 'class-validator';
@ValidatorConstraint({ name: 'isNumberArray', async: false })
export class IsNumberArrayConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
return this.isValid(value);
}
private isValid(arr: any[]): boolean {
for (const elem of arr) {
if (Array.isArray(elem)) {
// Recursive check for nested arrays
if (!this.isValid(elem)) {
return false;
}
} else if (typeof elem !== 'number') {
return false;
}
}
return true;
}
defaultMessage(args: ValidationArguments) {
return 'Each element of the array must be a number.';
}
}
위 코드는 들어오는 N차원 배열에 대해 재귀적으로 모든 배열의 요소가 number 타입인지 확인하는 유효성 검사 제약조건을 구현한 예제입니다.
export class GeometryClass {
@IsString()
@IsNotEmpty()
type: string;
@IsNotEmpty()
@Validate(IsNumberArrayConstraint)
coordinates: number | number[] | number[][] | number[][][] | number[][][][];
}
이후 위의 DTO Class의 geometry 좌표를 표현하기 위한 coordinates라는 필드에 해당 @Validate 데코레이터를 통해 유효성검사 로직을 적용하였습니다.
원하는 제약조건 생성을 위한 주요 class-validator의 인터페이스 및 클래스
원하는 제약조건 생성 방법에 대해 좀 더 자세히 알아보도록 하겠습니다.
기본적으로 class-validator 패키지에서 제공하는 @ValidatorConstraint, @Validate, ValidatorConstraintInterface 을 이용해서 나만의 Validation 데코레이터를 구축할 수 있습니다.
@ValidatorConstraint
@ValidatorConstraint({ name: 'customText', async: false })
- 기본적인 Validation에 대한 메타데이터를 설정할 때 사용됩니다.
- 기본적으로 꼭 사용될 필요는 없습니다. (생략 시 자동 생성됨)
- name : ValidationError 발생 시 에러 타입으로 사용됩니다.
- async : 비동기 함수일 때 true를 명시하여 줍니다.
ValidatorConstraintInterface
export class CustomTextValidator implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
// Custom validation logic
const isValid = /* Your validation logic here */;
return isValid;
}
defaultMessage(args: ValidationArguments) {
// Custom error message
return 'Custom validation failed';
}
}
- 기본적인 유효성 검사 함수를 구현하도록 명시해 주는 인터페이스입니다.
- 해당 인터페이스가 구현되면 validate 함수와 defaultMessage 함수를 구현해야 합니다.
- validate : 실제 유효성 검사에 대한 비지니스 로직이 들어갑니다.
- defaultMessage : 유효성검증이 실패하면 보여줄 메세지를 지정합니다.
@Validate
export class Post {
@Validate(CustomTextLength, [3, 20], { // constraint 전달
message: 'Wrong post title',
})
title: string;
}
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint()
export class CustomTextLength implements ValidatorConstraintInterface {
validate(text: string, validationArguments: ValidationArguments) {
// 전달받은 constraints를 통해 유효성 검사 수행
return text.length > validationArguments.constraints[0] && text.length < validationArguments.constraints[1];
}
}
- 최종적으로 @Validate 데코레이터를 생성하는 함수입니다. 해당 데코레이터를 구현한 필드에 대해 유효성 검사가 수행됩니다.
- @Validate 데코레이터는 첫 번째 파라미터로 ValidatorConstraintInterface 를 구현한 Class가 입력됩니다.
- 두 번째 인자로 추가적인 값을 제약조건이 구현된 클래스로 전달할 수도 있습니다. 해당 값은 원하는 제약조건에 대한 설정값을 직접 입력받을 때 사용됩니다. 아래 예제에서는 [3, 20] 을 전달하여 해당 범위의 크기에 해당되는 문자열만 입력 설정하였습니다.
Custom Validation 데코레이터 생성해 보기
@Validate 데코레이터를 통해 제약조건을 적용하여 유효성검사를 수행할 수도 있지만 나만의 이름을 가진 Custom 한 데코레이터를 생성하여 유효성검사를 수행할 수도 있습니다. 앞서 설명드린 로직과 90%는 동일하고, 단지 차이점은 registerDecorator라는 함수로 감싸주기만 하면 됩니다.
export function IsNumberArray(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'isNumberArray',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate(arr: any[], args: ValidationArguments) {
for (const elem of arr) {
if (Array.isArray(elem)) {
// Recursive check for nested arrays
if (!this.isValid(elem)) {
return false;
}
} else if (typeof elem !== 'number') {
return false;
}
}
return true;
},
defaultMessage(args: ValidationArguments) {
return 'Default message for ' + propertyName;
},
},
});
};
}
export class GeometryClass {
@IsString()
@IsNotEmpty()
type: string;
@IsNumberArray({ message: 'Each element of the array must be a number.' })
@IsNotEmpty()
coordinates: number | number[] | number[][] | number[][][] | number[][][][];
}
이후에 위와 같이 @IsNumberArray라는 Custom Validation 데코레이터를 통해 유효성 검사를 수행할 수 있습니다.
마무리
오늘은 NestJS에서 원하는 제약조건을 설정하는 방법과 Custom 한 이름을 유효성 검사 데코레이터를 생성하는 방법에 대해 알아보았습니다.
회사에서 express기반의 백엔드 코드들을 NestJS를 전환하게 되었는데, 초반에는 조금 사용법 및 구조를 파악하는데 어려웠는데요, 하지만 쓰면 쓸수록 확장성과 유지보수성에 뛰어나고 협업에도 좋은 프레임워크라고 생각이 드는 것 같습니다.
앞으로도 NestJS에 대한 내용을 포스팅 해보도록 하겠습니다.
감사합니다.
참고
'서버 인프라, 백엔드 > Nodejs, PM2' 카테고리의 다른 글
PM2 : logrotate 모듈을 이용하여 PM2 로그 용량 줄이기 (0) | 2024.01.08 |
---|---|
pm2 : 1개의 cluster에서만 cronjob 수행하기 (instance_var 옵션) (0) | 2023.07.23 |
PM2 : 무중단 서비스 배포 적용하기 (graceful reload) (0) | 2023.07.11 |
nodejs의 모듈 시스템 : export, import (0) | 2018.09.27 |