NestJS Pipes

paduck·2024년 6월 12일
0

NestJS

목록 보기
8/24

@Injectable() 데코레이터가 달린 클래스이며, implements PipeTransform 인터페이스

파이프는 두 가지 주요 용도로 사용

  • 변환: 입력 데이터를 원하는 형태로 변환 (예: 문자열->정수)
  • 검증: 입력 데이터를 평가하고 유효하면 그대로 통과시키고, 그렇지 않으면 예외를 던짐

두 경우 모두 파이프는 컨트롤러 라우트 핸들러에서 처리되는 arguments에 대해 작동

  • 메서드가 호출되기 직전에 파이프를 삽입
  • 이후 변환된 인자가 있는 상태로 라우트 핸들러가 호출

파이프는 예외 구역 내에서 실행됩니다. 즉, 파이프가 예외를 던지면 예외 레이어(전역 예외 필터 및 현재 컨텍스트에 적용된 모든 예외 필터에 의해 처리됩니다. 따라서 파이프에서 예외가 발생하면 컨트롤러 메서드는 실행되지 않습니다. 이는 외부 소스에서 애플리케이션으로 들어오는 데이터를 유효성 검사하는 최선의 방법을 제공합니다.

Built-in pipes

Nest에는 기본적으로 사용할 수 있는 아홉 가지 파이프가 있습니다:

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

이 파이프들은 @nestjs/common 패키지에서 사용

Binding pipes

파이프 인스턴스를 적절한 컨텍스트에 바인딩해야 사용 가능

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}
  • 매개 변수가 숫자여야 하고, 메서드 실행 전에 호출

다음의 호출에

GET localhost:3000/abc

다음의 예외가 발생

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

마찬가지로, 클래스를 전달하면 프레임워크가 인스턴스화 책임을 지고 의존성 주입을 활성화

  • 혹은, 파이프와 가드는 인라인 인스턴스를 전달 가능
  • 인라인 인스턴스를 전달하면 내장된 파이프의 동작을 옵션을 통해 커스터마이징 가능
@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

ParseUUIDPipe()를 사용할 때 버전 3, 4 또는 5의 UUID를 파싱합니다. 특정 버전의 UUID만 필요하다면 파이프 옵션에 버전을 전달할 수 있습니다.

유효성 검사 기술에서 검증 파이프의 광범위한 예제를 참조하세요.

Custom pipes

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

PipeTransform<T, R>는 모든 파이프가 구현해야 하는 제네릭 인터페이스입니다. 제네릭 인터페이스는 입력 value의 타입을 나타내는 Ttransform() 메서드의 반환 타입을 나타내는 R을 사용합니다.

모든 파이프는 PipeTransform 를 위해 transform() 메서드를 구현해야 함

  • value
    • 현재 처리 중인 메서드 인자(라우트 핸들러 메서드가 받기 전의 인자)
  • metadata
    • 현재 처리 중인 메서드 인자의 메타데이터
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
  • 이 속성들은 현재 처리 중인 인자를 설명
type 인자가 body @Body(), query @Query(), param @Param() 또는 custom 파라미터인지 여부를 나타냅니다. (자세한 내용은 여기를 참조하세요).
metatype 인자의 메타타입을 제공하며, 예를 들어 String입니다. 주의: 라우트 핸들러 메서드 서명에서 타입 선언을 생략하거나 바닐라 JavaScript를 사용할 경우 값이 undefined입니다.
data 데코레이터에 전달된 문자열이며, 예를 들어 @ Body('string')입니다. 데코레이터 괄호를 비워두면undefined입니다.

TypeScript 인터페이스는 트랜스파일 동안 사라집니다. 따라서 메서드 매개변수의 타입이 클래스가 아닌 인터페이스로 선언되면 metatype 값은 Object가 됩니다.

Schema based validation

post 의 body 객체에 대한 확인 예제;

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

create 메서드 요청이 유효한 본문을 포함하고 있는지 확인하려면, dto 객체의 세 가지 멤버를 검증

  • 라우트 핸들러 메서드 내부에서 수행 시 단일 책임 원칙(SRP) 불충족

따라서, 검증 클래스를 작성 후 검증 작업을 위임

  • 각 메서드의 시작 부분에서 호출해야 한다는 단점

혹은, 검증 미들웨어

  • 실행 컨텍스트(호출될 핸들러 및 해당 매개변수를 포함하는)를 인식하지 못하는 미들웨어의 특성상 일반적인 미들웨어를 작성할 수 없

Object schema validation

깔끔하고 DRY 방식으로 수행하는 여러 접근 방식 중, 스키마 기반 검증이 일반적

Zod 라이브러리는 간단한 API로 스키마를 작성할 수 있

$ npm install --save zod
  • schema.parse() 메서드를 적용하여 제공된 스키마에 대해 들어오는 인수를 검증

앞서 언급했듯이, 검증 파이프는 값을 변경하지 않고 반환하거나 예외를 던짐

import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema  } from 'zod';

export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      const parsedValue = this.schema.parse(value);
      return parsedValue;
    } catch (error) {
      throw new BadRequestException('Validation failed');
    }
  }
}

Binding validation pipes

ZodValidationPipe를 사용 단계:

  1. ZodValidationPipe 인스턴스를 생성
  2. 파이프의 클래스 생성자에 컨텍스트별 Zod 스키마를 전달
  3. 파이프를 메서드에 바인딩
  • Zod 스키마 예제:
import { z } from 'zod';

export const createCatSchema = z
  .object({
    name: z.string(),
    age: z.number(),
    breed: z.string(),
  })
  .required();

export type CreateCatDto = z.infer<typeof createCatSchema>;

@UsePipes() 데코레이터의 구현:

@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@UsePipes() 데코레이터는 @nestjs/common 패키지에서 가져옵니다.

zod 라이브러리는 tsconfig.json 파일에서 strictNullChecks 구성을 활성화해야 합니다.

Class validator

이 섹션의 기술은 TypeScript를 필요로 하며 바닐라 JavaScript로 작성된 애플리케이션에서는 사용할 수 없습니다.

class-validator 라이브러리와 잘 작동

  • 데코레이터 기반 검증 가능
  • metatype에 접근할 수 있기에 Pipe 기능과 결합할 때 매우 강력
$ npm i --save class-validator class-transformer

CreateCatDto 클래스가 Post 본문 객체의 단일 진리의 원천으로 유지(별도의 검증 클래스를 작성할 필요가 없음)

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

class-validator 데코레이터에 대해 자세히 알아보려면 여기를 참조하세요.

ValidationPipe 클래스를 작성

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) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    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);
  }
}

직접 일반적인 검증 파이프를 작성할 필요는 없습니다. Nest는 기본적으로 ValidationPipe를 제공합니다. 내장된 ValidationPipe는 우리가 작성한 샘플보다 더 많은 옵션을 제공하며, 자세한 내용과 많은 예제를 여기에서 확인할 수 있습니다.

위에서는 class-transformer 라이브러리를 사용했습니다. 이 라이브러리는 class-validator 라이브러리와 같은 저자가 작성했기 때문에 상호 호환성이 좋습니다.

  1. transform() 메서드가 async
  • 동기 및 비동기 파이프가 가능하므로 async를 활용
  • 일부 class-validator 검증이 비동기일 수 있기 때문(Promise를 활용)
  1. 구조 분해 할당으로 ArgumentMetadata에서 metatype 필드 추출

  2. 헬퍼 함수 toValidate()

  • 기본 JavaScript 타입은 검증 단계 생략(기본 JavaScript 타입에는 검증 데코레이터를 붙일 수 없기 때문)
  1. 일반 JavaScript 인수 객체를 plainToInstance() 함수로 타입이 지정된 객체로 변환하여 검증을 수행
  • 네트워크 요청에서 역직렬화된 들어오는 post 본문 객체는 타입 정보가 전혀 없음 (Express와 같은 기본 플랫폼의 작동 방식)
  • class-validator는 우리가 DTO에 대해 이전에 정의한 검증 데코레이터를 사용해야 하므로, 들어오는 본문을 단순한 객체가 아닌 적절한 데코레이터가 적용된 객체로 처리하기 위해 이 변환이 필요
  1. 검증 파이프는 값을 변경하지 않고 반환하거나 예외를 던

  2. ValidationPipe를 바인딩

  • 매개변수 범위, 메서드 범위, 컨트롤러 범위 또는 전역 범위로 바인딩

파이프 인스턴스를 라우트 핸들러 @Body() 데코레이터에 바인딩하여 파이프가 post 본문을 검증하게 함

@Post()
async create(
  @Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
  this.catsService.create(createCatDto);
}

Global scoped pipes

전역 범위 파이프로 설정하여 애플리케이션 전체의 모든 라우트 핸들러에 적용 가능

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

하이브리드 앱의 경우 useGlobalPipes() 메서드는 게이트웨이 및 마이크로서비스에 대해 파이프를 설정하지 않습니다. "표준" (비하이브리드) 마이크로서비스 앱의 경우, useGlobalPipes()는 파이프를 전역적으로 마운트합니다.

  • 전역 파이프는 전체 애플리케이션에 대해 모든 컨트롤러 및 모든 라우트 핸들러에 사용

전역 파이프가 모듈 외부에서 등록될 경우(위 예제에서처럼 useGlobalPipes()를 사용하여) 의존성을 주입할 수 없음

  • 모듈의 컨텍스트 외부에서 이루어졌기 때문

모듈 내부에서 직접 전역 파이프를 설정하여 해결 가능:

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}

이 방법을 사용하여 파이프에 대해 의존성 주입을 수행할 때, 이 구성이 사용된 모듈에 관계없이 파이프는 실제로 전역적임을 유의하세요. 어디에서 해야 할까요? 파이프(위 예제에서는 ValidationPipe)가 정의된 모듈을 선택하세요. 또한, useClass는 커스텀 제공자 등록을 처리하는 유일한 방법이 아닙니다. 자세한 내용은 여기에서 확인하세요.

The built-in ValidationPipe

직접 일반적인 검증 파이프를 작성할 필요는 없고,ValidationPipe를 제공
내장된 ValidationPipe는 우리가 작성한 샘플보다 더 많은 옵션을 제공하며, 자세한 내용과 많은 예제를 여기에서 확인 가능

Transformation use case

입력 데이터를 원하는 형식으로 변환도 가능
transform 함수에서 반환된 값이 인자의 이전 값을 완전히 덮어쓰므로 가능

변환 파이프는 타입 변환 혹은 기본값 적용 등의 작업을 수행하여 클라이언트 요청과 요청 핸들러 사이에 처리 함수를 삽입 가능

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

바인딩은:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return this.catsService.findOne(id);
}

Providing defaults

Parse* 파이프는 매개변수 값이 정의되어 있을 것으로 예상(기대)

  • null 또는 undefined 값은 예외
  • 누락의 경우를 처리하려면 Parse* 파이프가 이러한 값에 대해 작동하기 전에 기본 값을 제공

DefaultValuePipe를 사용하여, Parse* 파이프가 작동하기 전에 @Query() 데코레이터에서 DefaultValuePipe 인스턴스를 인스턴스:

@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
학습 velog

0개의 댓글