ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] class-validator의 활용과 검증 옵션
    Javascript, Typescript 2022. 1. 3. 22:21
    반응형

    class-validatorjoi의 Typescript 버전으로, 데코레이터를 이용해서 편리하게 오브젝트의 프로퍼티를 검증할 수 있는 라이브러리이다.

    웹 서버에서 들어오는 HTTP 요청의 JSON body 검증할 때 굉장히 유용한데, Nest.js에서는 빌트인 ValidationPipe가 이 두 라이브러리를 이용한다.

    이번에는 class-validator의 활용 방법과 검증 옵션을 알아보자.

    기본 사용법

    import { IsEmail, IsOptional, IsString, Min } from 'class-validator';
    
    class CreateUserDto {
      @IsString()
      name: string;
    
      @Min(1)
      age: number;
    
      @IsEmail()
      @IsOptional()
      email?: string;
    }
    import { validateOrReject } from 'class-validator';
    
    const createUserDto = new CreateUserDto();
    createUserDto.name = 'Kim';
    createUserDto.age = 28;
    
    validateOrReject(createUserDto); // 통과

    class 스키마를 정의하고, 프로퍼티의 유효성을 검증할 적절한 데코레이터를 달아주는 방식으로 사용한다. 다양한 검증 규칙 데코레이터들에 관한 설명은 공식 문서를 참고하자.

    웹 프레임워크에서

    express나 Nest.js 같은 웹 프레임워크에서는 요청의 JSON body를 검증하는데 사용할 수 있다. 다음과 같이 class-transformerclass-validator를 이용해서 라우터나 컨트롤러에 도달하기 전에 요청의 JSON body를 클래스의 인스턴스로 변환한 뒤에 검증할 수 있다.

    Nest.js

    Nest.js에서는 기본 ValidationPipe를 사용하면 되는데, 다음 두가지 방법 중 하나를 이용해서 Global pipe로 등록하거나 App 모듈의 APP_PIPE라는 Provider로 등록할 수 있다.

    App 모듈의 provider로 등록한다면, 의존성 주입을 활용할 수 있다는 장점이 있다.
    import { ..., ValidationPipe } from '@nestjs/common';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      
      // app 인스턴스 생성 시 Global pipe로 등록
      app.useGlobalPipes(
        new ValidationPipe()
      );
    }
    
    bootstrap();
    import { ..., ValidationPipe } from '@nestjs/common';
    import { APP_PIPE } from '@nestjs/core';
    
    // App 모듈에서 provider로 등록
    @Module({
      providers: [
        {
          provide: APP_PIPE,
          useClass: ValidationPipe, // 옵션설정을 위해 useValue, useFactory 등 사용 가능
        },
      ],
    })
    export class AppModule {}
    // Controller에서 사용
    @Post()
    create(@Body() createUserDto: CreateUserDto) {
      // 검증이 완료된 request body 이용
    }

    express

    express를 사용할 때에는 이 부분을 검증할 클래스 스키마를 인자로 받는 미들웨어로 만들어서 처리할 수 있다.

    검증에 성공하면 plain object인 요청 body를 변환된 클래스의 인스턴스로 새로 할당해서 다음 미들웨어로 넘겨줄 수 있다.

    import { validateOrReject } from 'class-validator';
    import { plainToClass } from 'class-transformer';
    
    // 미들웨어 정의
    export function validateBody(schema: { new (): any }) {
      return async function (req: Request, res: Response, next: NextFunction) {
        const target = plainToClass(schema, req.body);
        try {
          await validateOrReject(target);
          next();
        } catch (error) {
          next(error);
        }
      };
    }
    // Router에서 사용
    router.post('/users',
      validateBody(CreateUserDto),
      (req: Request, res: Response) => {
        // 검증이 완료된 request body 이용
      }
    );

    검증 규칙

    검증에 사용하는 옵션 중 자주 사용하는 주요 옵션 4가지가 있는데 원하는 규칙에 따라 적용할 수 있는 옵션이 다르다.

    skipMissingProperties

    class-validator는 검증 데코레이터가 붙은 프로퍼티가 대상 오브젝트에 존재하지 않으면 오류를 낸다.(IsOptional() 인 프로퍼티 제외)

    skipMissingProperties 옵션은 검증 데코레이터가 있고, IsOptional이 아닌 프로퍼티가 대상 오브젝트에 존재하지 않더라도 에러가 나지 않게 해주는 옵션이다.

    const createUserDto = new CreateUserDto();
    createUserDto.name = 'Kim';
    // createUserDto.age = 28;
    
    validateOrReject(createUserDto, { skipMissingProperties: true }); // 통과
    validateOrReject(createUserDto); // ValidationError: age must not be less than 1

    forbidUnknownValues

    이름만 봤을 때는 어떤 규칙인지 잘 감이 오지 않는다. 이 옵션은 프로퍼티에 검증 규칙이 정의되어있지 않은 클래스의 인스턴스나, plain object를 검증할 때 오류가 나게 만드는 옵션이다.

    이 옵션이 없으면 프로퍼티에 규칙이 정해지지 않은 class 스키마나 plain object도 검증을 통과하기 때문에 공식 문서에서 이 옵션을 true로 설정하는 것을 강력히 권장하고 있다.

    class UnknownValue {
      name: string;
      age: number;
    }
    
    const unknownValue = new UnknownValue();
    unknownValue.name = 'Kim';
    unknownValue.age = 28;
    
    validateOrReject(unknownValue); // 통과
    validateOrReject(unknownValue, { forbidUnknownValues: true }); // ValidationError: an unknown value was passed to the validate function

    관련 issue에서도 논의되는 것처럼, forbidPlainObjectforbidUnknownSchema 등의 다른 이름이 적절하지 않았을까 싶다.

    whitelist

    class-validator는 검증을 수행할 때 대상 객체에 검증 규칙이 정의되어있지 않은 프로퍼티가 있더라도 오류를 내지 않고 그대로 통과시켜준다.

    whitelist 옵션은 검증을 통과한 뒤, 대상 객체에서 검증 규칙이 정의되어있지 않은 프로퍼티를 모두 제거해주는 옵션이다.

    어떤 형태의 JSON body가 들어올지 모르는 요청 body를, 검증 이후에 예측 가능한 객체 형태로 만들어줄 수 있기 때문에 유용하다.

    const createUserDto = new CreateUserDto();
    createUserDto.name = 'Kim';
    createUserDto.age = 28;
    (createUserDto as any).blog = 'Tistory';
    
    await validateOrReject(createUserDto);
    console.log(createUserDto); // CreateUserDto { name: 'Kim', age: 28, blog: 'Tistory' }
    
    await validateOrReject(createUserDto, { whitelist: true });
    console.log(createUserDto); // CreateUserDto { name: 'Kim', age: 28 }

    forbidNonWhitelisted

    whitelist 옵션과 같이 사용하는 옵션으로, 대상 객체에 검증 규칙이 정의되어있지 않은 프로퍼티가 있으면 오류를 내게 하는 옵션이다.

    const createUserDto = new CreateUserDto();
    createUserDto.name = 'Kim';
    createUserDto.age = 28;
    (createUserDto as any).blog = 'Tistory';
    
    validateOrReject(createUserDto, { whitelist: true, forbidNonWhitelisted: true }); // ValidationError: 'property blog should not exist'

    Nest.js에서 검증 규칙 사용하기

    Nest.js에서는 ValidationPipe의 인스턴스를 생성할 때, class-validator의 옵션들을 생성자의 매개변수로 넘겨줄 수 있다.

    // app 인스턴스 생성 시 Global pipe로 등록할 때
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
      }),
    );
    // App 모듈에서 provider로 등록할 때
    @Module({
      providers: [
        {
          provide: APP_PIPE,
          useValue: new ValidationPipe({
            whitelist: true,
          }),
        },
      ],
    })
    export class AppModule {}

    Conclusion

    class-validator는 어떤 형태로 요청이 들어올지 모르는 요청 body를 일차적으로 검증해줄 수 있는 라이브러리이다.

    요구사항에 따라 적절한 데코레이터와 적절한 옵션을 조합해서 다양한 오브젝트를 검증해 안전한 서버 사이드 어플리케이션을 개발할 수 있도록 하자.

    반응형

    댓글

Designed by Tistory.