[NestJs] Class-Validator 로 DTO 타입 체크하면서 undefined를 받는 방법

Jung Ian·2021년 11월 21일
1

nestjs

Class-Validator 데코레이터(어노테이션) 설명

요약

  • @IsOptional()를 이용하면 undefined를 받을 수 있으면서 값이 존재할 때는 @IsString(), @IsNumber() 등으로 타입 체크도 가능하다.
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';

export class RequestDto {
  @ApiProperty({
    description: 'name1 field'
  })
  @IsNotEmpty()
  @IsString()
  public name1: string;

  @ApiProperty({
    description: 'name2 field'
  })
  @IsNotEmpty()
  @IsString()
  public name2: string;

  @ApiProperty({
    description: 'name3 field'
  })
  @IsOptional()
  @IsString()
  public name3: string;
}

과정

문제 발견, 인식

최근 새롭게 추가하고 있는 기능의 API에서 파라미터로 dto를 이용해 데이터를 받도록 만들고 있었다.
문제는 dto의 필드들 중 1개는 필수값이 아니었는데, 이 값이 없을 경우, undefined로 받아 DynamoDB에 저장할 때 레코드의 속성이 생성되지 않도록 하고 싶었다.

따라서 아래의 코드처럼 DTO를 정의하고 class-validator의 각종 데코레이터들을 이용해 유효성 체크 설정까지 했다.

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';

export class RequestDto {
  @ApiProperty({
    description: 'name1 field'
  })
  @IsNotEmpty()
  @IsString()
  public name1: string;

  @ApiProperty({
    description: 'name2 field'
  })
  @IsNotEmpty()
  @IsString()
  public name2: string;

  @ApiProperty({
    description: 'name3 field'
  })
  @IsString()
  public name3: string;
}

name3 필드의 경우 필수값이 아니기에 @IsNotEmpty() 데코레이터 설정을 하지 않고 숫자가 들어오면 안되므로 @IsString() 데코레이터으로 string 타입 체크만 하도록 설정했다.

그리고 아래와 같이 프론트 측에서 매개 변수를 생성하여 해당 API를 호출하였다.

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: 'NAME2 FIELD',
    name3: undefined
  };

결과는 400에러... 필수값이 아닌 name3의 유효성 체크가 되고 있었다.

{
  "statusCode": 400,
  "message": [
    "name3 must be a string"
  ],
  "error": "Bad Request"
}

dto 정의에서 name3 필드 정의를 잘못했나 하여 null 이나 undefined가 허용되도록 [?]를 붙여 다시 호출해 보았다.


  @ApiProperty({
    description: 'name3 field'
  })
  @IsString()
  public name3?: string;

결과는 또 400에러... 계속해서 name3에 대한 체크가 실행된다.

{
  "statusCode": 400,
  "message": [
    "name3 must be a string"
  ],
  "error": "Bad Request"
}

문제 원인 분석

필수값인 name2를 undefined로 하여 테스트 해 보았다.

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: undefined,
    name3: 'NAME3 FIELD'
  };

결과는 마찬가지로 400에러이지만 에러 메세지가 name3 때와 다르다.
name2는 @IsNotEmpty() 와 @IsString() 2개의 데코레이터로 유효성 체크를 하고 있는데 "name2 should not be empty" 란 에러 메세지가 추가적으로 나오는 것을 볼 수 있다.

{
  "statusCode": 400,
  "message": [
    "name2 must be a string",
    "name2 should not be empty",
  ],
  "error": "Bad Request"
}

혹시나 하는 마음에 name2 필드에 빈 문자열(length === 0)을 넣어 테스트 해 보았다.

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: '',
    name3: 'NAME3 FIELD'
  };

결과를 통해서 에러 메세지가 1개만 나온 것을 확인할 수 있었다.

{
  "statusCode": 400,
  "message": [
    "name2 should not be empty",
  ],
  "error": "Bad Request"
}
  • @IsString() : "name2 must be a string"
  • @IsNotEmpty() : "name2 should not be empty"

이 테스트를 통해 @IsString() 데코레이터는 string이 아닌 모든 값을 체크하고 있기 때문에 객체, 숫자를 포함해 null이나 undefined 값을 보낼 수가 없다는 것을 알게 되었다.

단순히 @IsNotEmpty() 데코레이터를 쓰지 않으면 간단하게 undefined를 API로 보낼 수 있다고 생각했는데 생각지도 못한 것에 막혔다. (필드 정의에서 [?]는 유효성 체크에서는 아무 의미가 없었다.)

이것 저것 찾아보다가 nest 공식 문서에서 찾은 아래의 내용을 이용해 다시 테스트 해보았다.

HINT
Instead of explicitly typing the @ApiProperty({ required: false }) you can use the @ApiPropertyOptional() short-hand decorator.

name3를 아래와 같이 변경하고, name3에 undefined를 넣어 API를 호출 해 보았다.

  @ApiProperty({
    description: 'name3 field',
    required: false,
  })
  @IsString()
  public name3: string;

결과는 변화없음...

{
  "statusCode": 400,
  "message": [
    "name3 must be a string"
  ],
  "error": "Bad Request"
}

현재 상태에서 이를 해결하기 위해서는 3가지 정도의 방법이 있다.
방법1. API를 호출 할 때, name3에 빈 문자열을 담아서 보낸다.

  • dynamoDB에 저장하면 name3 속성이 생성되고 빈 문자열이 들어간다.(원하지 않음)

방법2. 방법1의 방법에서 DynamoDB에 저장할 때, name3가 빈 문자열인 경우 undefined로 변경하여 저장한다.

  • name3 ? undefined : name3
  • 결과는 만족스러우나 쓸데없는 삼항연산식 처리가 들어가야 한다.

방법3. dto 정의에서 @IsString() 데코레이터를 사용하지 않는다.

  • API를 호출하는 쪽에 문자열 타입 체크를 하고 있기 때문에 기능상 아무런 문제가 없다. 다만, 파라미터에 대한 유효성 체크를 호출하는 쪽에 전적으로 맡기는 것이 불안하다.

모두 마음에 들지는 않지만 결국 아래와 같이 name3의 값이 undefined인 경우 빈 문자열을 넣어서 보내는 것으로 해결하기로 했다. (방법2)

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: 'NAME2 FIELD',
    name3: ''
  };

문제 해결

하지만 이건 아무리 생각해도 아닌 것 같고 오기가 생겨 좀더 찾아보기로 했는데, class-validator공식 문서에는 뭔가 방법이 있지 않을까 해서 마지막으로 찾아보기로 했다.

역시 공식 사이트에는 전체 데코레이터들에 대한 소개가 있었고, 내가 원하는 방식과 가장 비슷한 설명이 있는 것은 한 번씩 테스트 해 보며 찾았다.

그 중 내 눈에 띄는 한 개의 데코레이터 소개.

@IsOptional()
Checks if given value is empty (=== null, === undefined) and if so, ignores all the validators on the property.
해당 필드의 값을 체크하여 null이나 undefined의 경우, 해당 필드의 다른 데코레이터들을 무시한다.

@IsOptional()를 사용해보자!

dto의 name3 필드를 아래와 같이 변경했다.

  @ApiProperty({
    description: 'name3 field',
  })
  @IsOptional()
  @IsString()
  public name3: string;

그리고 API를 매개 변수의 name3에 undefined를 넣어 호출하였다.

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: 'NAME2 FIELD',
    name3: undefined
  };

결과는 OK!

{
  "name1": "NAME1 FIELD",
  "name2": "NAME2 FIELD"
}

내친김에 문자열(string) 체크도 해 본다. name3에 숫자를 넣어 보내본다. 에러가 나야한다.

  const result: RequestDto = {
    name1: 'NAME1 FIELD',
    name2: 'NAME2 FIELD',
    name3: 12345
  };

결과는 400에러!

{
  "statusCode": 400,
  "message": [
    "name3 must be a string"
  ],
  "error": "Bad Request"
}

값이 존재할 경우에는 @IsString() 데코레이터에 의한 문자열 체크가 실행되고,
값이 존재하지 않을 경우에는 유효성 검사를 하는 다른 데코레이터들의 기능이 작동하지 않는다.

내가 찾던 데코레이션이었다.

@IsOptional()

혹시나 해서 @IsNotEmpty() 데코레이터도 함께 사용해보았다.

  @ApiProperty({
    description: 'name3 field',
  })
  @IsOptional()
  @IsNotEmpty()
  @IsString()
  public name3: string;

이 경우에도 정상적으로 작동했다. 값이 존재하지 않을 경우에는 다른 데코레이터들이 작동하지 않았고, 값이 존재할 경우에는 문자열 타입 체크와 함께 빈 문자열 체크가 추가되었다.


공식 문서를 먼저 찾아보자!

profile
Tokyo Dev

0개의 댓글