Pipe

이준엽·2024년 12월 14일
  1. Pipes

    • pipe 사용 예시
      • transformation - 입력 데이터를 원하는 출력으로 변환
      • validation - 유효성 검사
  2. Built-in pipes

    • ValidationPipe
    • ParseIntPipe
    • ParseFloatPipe
    • ParseBoolPipe
    • ParseArrayPipe
    • ParseUUIDPipe
    • ParseEnumPipe
    • DefaultValuePipe
    • ParseFilePipe
  3. Binding pipes

    • 매개 변수 수준에서 pipe를 바인딩 할 수 있다.
      @Get(':id')
      async findOne(@Param('id', ParseIntPipe) id: number) {
        return this.catsService.findOne(id);
      }
    • 클래스가 아닌 인스턴스(new)화 하여 사용 환경마다 커스터마이징 해서 사용할 수 있음.
      @Patch('bulk')
        async updateBulkCoupons(
          @Body(new ParseArrayPipe({ items: PatchCouponsRequestDto }))
          patchCouponsRequestDto: PatchCouponsRequestDto[],
        ) { ... }
      }
  4. Custom pipes

    • pipeTransform interface를 기준으로 클래스 생성
    • transform(value, metadata) 메서드를 정의
      • value - 현재 처리 된 인수
      • metadata - 처리되는 인수의 메타데이터
        export interface ArgumentMetadata {
        	readonly type: 'body' | 'query' | 'param' | 'custom'; 
        	readonly metatype?: Type<any>;
        	readonly data?: string;
        }
    • ts 컴파일과정에서 인터페이스는 사라진다. metatype이 object가 되므로 클래스를 권장함.
    @Controller('cats')
    export class CatsController {
      @Post()
      async create(@Body() createCatDto: CreateCatDto) {
        // DTO를 클래스로 사용했으므로 이후의 작업에 필요한 타입 정보를 활용할 수 있습니다.
      }
    }
  5. Object schema validation

    • 라우트 핸들러 메소드 내에서 수행하기엔 SRP(단일 책임 규칙)를 위반
    • 미들웨어를 활용하기엔 execution context에 접근할 수 없기에 비추천
    • 스키마 기반 유효성 검사(Joi 활용법)
      import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
      import { ObjectSchema } from 'joi';
      
      @Injectable()
      export class JoiValidationPipe implements PipeTransform {
        constructor(private schema: ObjectSchema) {}
      
      transform(value: any, metadata: ArgumentMetadata) {
          const { error } = this.schema.validate(value);
          if (error) {
            throw new BadRequestException('Validation failed');
          }
          return value;
        }
      }
      
      • Binding validation pipes
        • Joi 스키마
          const createCatSchema = Joi.object({
            name: Joi.string().required(),
            age: Joi.number().required(),
            breed: Joi.string().required(),
          })
          export interface CreateCatDto {
            name: string;
            age: number;
            breed: string;
          }
        • @UsePipes() 를 사용
          @Post()
          @UsePipes(new JoiValidationPipe(createCatSchema))
          async create(@Body() createCatDto: CreateCatDto) {
            this.catsService.create(createCatDto);
          }
  6. Class validator

    • 데코레이터 기반 유효성 검사
    import { IsString, IsInt } from 'class-validator';
    
    export class CreateCatDto {
      @IsString()
      name: string;
    	
    	@IsInt()
      age: number;
    	
    	@IsString()
      breed: string;
    }
    import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
    import { validate } from 'class-validator';
    import { plainToInstance } from 'class-transformer';
    
    @Injectable()
    export class ValidationPipe implements PipeTransform<any> {
      async transform(value: any, { metatype }: ArgumentMetadata) {
    		
    		// metatype이 존재하고 유효성 검사가 필요하다면, plainToInstance 함수를 이용하여 사용자로부터 받은 데이터(value)를 해당 metatype의 인스턴스로 변환.
    		
        if (!metatype || !this.toValidate(metatype)) {
          return value;
        }
    
        const object = plainToInstance(metatype, value);
    
    		// validate 함수를 이용하여 데코레이터 기반 유효성 검사실행
    	
        const errors = await validate(object);
        if (errors.length > 0) {
          throw new BadRequestException('Validation failed');
        }
        return value;
      }
    
      private toValidate(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype);
      }
    }
  7. Global scoped pipes

    • app.useGlobalPipes() 를 이용하여 등록
      • 하이브리드 애플리케이션(HTTP와 다른형태의 통신을 이용하는 경우) 다른 종류를 위한 pipe를 별도로 설정하여 데이터 구조를 처리하도록 할 수 있음
    • 위의 방식대로 등록 할 경우 module의 context 외부에서 등록하였기 때문에 DI가 불가능하다.
      밑의 방식으로 해결 할 수 있다.
      ```tsx
      import { Module } from '@nestjs/common';
      import { APP_PIPE } from '@nestjs/core';
      
      @Module({
        providers: [
          {
            provide: APP_PIPE,
            useClass: ValidationPipe,
          },
        ],
      })
      export class AppModule {}
      ```
    • monorepo 에서 사용중인 validation filter 옵션
    transform: true, // 파이프가 객체를 DTO클래스의 인스턴스로 자동 변환한다. 런타임에도 해당 타입의 안전성 보장
    whitelist: true, // DTO에 정의되지 않은 프로퍼티가 객체에 있는 경우 자동 제거.
    stopAtFirstError: true, // 오류가 발생하면 유효성 검사가 중지된다.
  8. The built-in ValidationPipe

    • 옵션 정리
      1. transform: 이 옵션은 Boolean 값으로, true로 설정하면 파이프가 객체를 DTO 클래스의 인스턴스로 자동 변환합니다. 이렇게 하면 런타임에도 해당 타입의 안전성을 보장할 수 있습니다.
      2. validateCustomDecorators: 이 옵션은 Boolean 값으로, true로 설정하면 사용자 정의 데코레이터에 대한 유효성 검사도 수행합니다.
      3. skipMissingProperties: 이 옵션은 Boolean 값으로, true로 설정하면 객체의 일부 프로퍼티가 누락된 경우 해당 프로퍼티를 검증에서 건너뜁니다.
      4. exceptionFactory: 이 옵션은 함수를 받아, 유효성 검증에서 에러가 발생했을 때 반환할 예외를 사용자가 직접 정의할 수 있게 해줍니다.
      5. whitelist: 이 옵션은 Boolean 값으로, true로 설정하면 DTO에 정의되지 않은 프로퍼티가 객체에 있을 경우 해당 프로퍼티를 자동으로 제거합니다.
      6. forbidNonWhitelisted: 이 옵션은 Boolean 값으로, true로 설정하면 DTO에 정의되지 않은 프로퍼티가 객체에 있을 경우 에러를 발생시킵니다.
      7. forbidUnknownValues: 이 옵션은 Boolean 값으로, true로 설정하면 유효성 검사 도중 알 수 없는 값이 발견될 경우 에러를 발생시킵니다.
      8. dismissDefaultMessages: 이 옵션은 Boolean 값으로, true로 설정하면 기본 유효성 검사 메시지를 비활성화합니다.
      9. validationError: 이 옵션은 { target: boolean, value: boolean } 형태의 객체를 받아, 각각 ValidationError 객체에 targetvalue 속성을 포함할지 여부를 지정합니다.
      10. groups: 이 옵션은 문자열의 배열을 받아, 그룹화된 속성에 대한 유효성 검사를 수행할 때 사용됩니다.
      11. always: 이 옵션은 Boolean 값으로, true로 설정하면 일부 조건에 따라 속성이 유효성 검사에서 제외될 때 이를 무시하고 항상 유효성 검사를 수행합니다.
      12. message: 이 옵션은 문자열이나 함수를 받아, 유효성 검사 에러 메시지를 재정의합니다.
      13. errorHttpStatusCode: 이 옵션은 숫자를 받아, 유효성 검사 에러가 발생
  9. Transformation use case

    • monorepo에서 찾은 사용 사례
    @Patch('bulk')
      async updateBulkCoupons(
        @Body(new ParseArrayPipe({ items: PatchCouponsRequestDto }))
        patchCouponsRequestDto: PatchCouponsRequestDto[],
      ) {
        const updatedCoupons = await this.couponsService.updateBulkCoupons(
          patchCouponsRequestDto,
        );
    
        return plainToInstance(
          PatchCouponsResponseDto,
          {
            data: updatedCoupons,
            totalCount: updatedCoupons.length,
          },
          { excludeExtraneousValues: true },
        );
      }
  10. Providing defaults

    • 기본값을 제공해줄때 사용하는 파이프(null, undefined값을 받으면 예외가 발생되기 떄문)
      @Get()
      async findAll(
        @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
        @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
      ) {
        return this.catsService.findAll({ activeOnly, page });
      }
profile
하루하루 차근차근

0개의 댓글