처음부터 차근차근

[NestJS] Pipe 본문

FrameWork/NestJS

[NestJS] Pipe

HangJu_95 2023. 11. 28. 12:03
728x90

Pipe

NestJS Pipe https://docs.nestjs.com/pipes

Pipe는 두 가지 역할을 합니다.

  • transformation: transform input data to the desired form (e.g., from string to integer)
  • validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception

파이프는 Route handler가 동작되기 전에 실행되며, route handler의 인자들이 도착하기 전에 작업을 처리합니다.

Nest의 Pipe를 통해 인자들의 유효성 검사를 진행하거나, 인수를 변환하는 작업을 진행합니다.

주의할 점은 파이프는 Exception 영역 내부에서 실행됩니다. 만약 유효성 검사를 진행하다 예외상황이 발생한다면 Exception filter에 의해 처리됩니다. 

따라서 Pipe에서 예외가 발생한다면 Controller method는 실행되지 않습니다.

 

Nest에서는 기본적인 내장 파이프를 제공합니다.

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

Binding Pipes

Pipe를 사용하려면 다음과 같은 예시로 적용할 수 있습니다.

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

 @Param를 통해 URL Parameter를 받고 있습니다. 

이를 ParseIntPipe Class를 통해 string으로 들어온 Param값을 Int로 변환시켜서 출력시켜줍니다.

ParseIntPipe 두 가지 기능을 실행합니다.

  • Param의 id 값이 숫자인가?
  • string으로 들어온 숫자 값을 Int타입으로 변환

위의 예시에서는 인스턴스가 아닌 클래스를 전달하여 인스턴스화에 대한 책임을 프레임 워크에 맡기고 의존성 주입을 활성화합니다.

만약 내부 인스턴스(상태코드, 상태 메세지 등)을 전달하기 위해서는 옵션을 사용하여 맞춤설정할 수 있습니다.

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

Param 뿐만 아니라 Query, Body 또한 가능합니다.

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

ParseUUIDPipe는 UUID 버전 3,4,5에서 UUID를 구문 분석하게 되는데, 만약 특정 버전의 UUID만 필요한 경우 옵션으로 정의할 수 있습니다.

Custom Pipes

Pipe를 우리의 입맛대로 Custom할 수 있습니다.

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

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

모든 파이프는 PipeTransform의 Transform 메소드를 구현해야 합니다. 이 메소드에는 기본적으로 두가지 매개 변수가 있습니다.

  •  value : 현재 pipe에서 어떠한 로직을 처리하고 route handler로 보내야 하는 값을 의미합니다.
  • metadata : 현재 pipe에 들어온 인수를 의미하며, 처리해야 되는 값을 의미합니다.
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

ArgumentMetadata 인터페이스에는 다음과 같은 속성이 있습니다.

  • Type: body인지, query인지, param 혹은 custom인지 판단합니다.
  • metatype : 인수의 메타 유형을 제공합니다. 예시로 String이 있슴니다. 바닐라 JS를 사용하는 경우에는 undefined입니다.
  • data: 데코레이터에 전달된 문자열입니다. 

Object schema validation

아래에 있는 DTO 예시를 한번 살펴보겠습니다.

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

위와 같은 클래스를 검증하려면 어떤 방법이 있을까요??

  • 핸들러 메소드 내부에서 검증할 수도 있지만, 단일 책임 원칙을 위반한다.
  • 검증기 클래스를 만들고 작업을 위임하는것이지만, 매번 클래스를 만들고 메소드 시작 시 유효성 검사기를 호출해야 한다.
  • 검증 미들웨어는 실행 컨텍스트를 인식하지 못하기 때문에 Fail

여러가지 방법 중 Schema 기반 유효성 검사가 있습니다. Zod 라이브러리를 사용하면 가능합니다.

(zod 라이브러리를 사용하려면 tsconfig.json에서 strictNullChecks 구성을 활성화 해야 합니다.)

Zod library를 통해 Pipe를 만들어보겠습니다.

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

export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodObject<any>) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      // zod 메서드를 통해 유효성 검사 진행
      this.schema.parse(value);
    } catch (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

아니면 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;
  }
}

이제 스키마를 한번 만들어보겠습니다.

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>;

 

이렇게 스키마를 만들고 type을 정의해줍니다.

이후 @UsePipe 데코레이터를 통해 Route handler에 적용시킬 수 있습니다.

@Post()
// ZodvalidationPipe를 적용시킨다.
// 인자에는 검사할 type을 명시
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Class validator

클래스를 통해서 검증할 수도 있습니다.

Nest는 class-validator 라이브러리와 잘 작동합니다. 특히 Pipe와 작동할 때 정말 간편합니다.

(따라서 Nest는 DTO를 만들 때 Class를 통해 생성하는 것이 편리합니다.)

 

아래와 같이 두가지 라이브러리를 설치합니다.

$ npm i --save class-validator class-transformer

이후 CreateCatDTO 클래스에 다양한 데코레이터를 추가할 수 있습니다.

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

export class CreateCatDto {
  // string인지 확인한다.
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

이제 이러한 데코레이터를 활용하는 ValidationPipe 클래스를 만들 수 있습니다.

ValidationPipe는 Nest에서 기본적으로 제공되므로 직접 구축할 필요가 없습니다. 아래에 있는 예시는 매커니즘을 설명하기 위해 작성된 예시입니다.

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로 표시, 일부 클래스는 비동기화가 될 수 있기 때문에
  // metatype 매개변수로 추출하기 위해 구조 분해를 사용하고 있다.
  async transform(value: any, { metatype }: ArgumentMetadata) {
    // 처리 중인 현재 인수가 Javascript인 경우, 유효성 검사 단계를 우회하는 역할
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    // plainToInstance 메소드로 일반 객체를 검증이 가능한 클래스로 변환시켜줍니다.
    // 일반 객체 Request에는 타입 정보와 데코레이터가 없기 때문입니다.
    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);
  }
}

마지막으로 파이프를 적용시켜보겠습니다

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

Globel scoped pipes

전역으로 바인딩 시키고 싶은 경우는 Guard, interceptor와 마찬가지로 두 가지 방법이 존재합니다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

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

The built-in ValidationPipe

Built-in ValidationPipe를 조금 더 확인하고 싶은 경우
https://docs.nestjs.com/techniques/validation

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

해당 Docs에 자세하게 나와있으며, 추후 정리예정입니다.

Transformation use case

유효성 검사 뿐만 아니라 변환에도 사용이 가능합니다.

간단한 예시로 ParseIntPipe를 만들어보겠습니다.

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;
  }
}

parseInt 함수를 통해서 작성하였으며, 만약 val 값이 NaN이면 에러 처리를 진행하였습니다.

이를 통해서 문자 -> 숫자열로 변환이 가능하며, 다양하게 변환할 수 있습니다.

참조

https://docs.nestjs.com/pipes

https://gongmeda.tistory.com/55?category=1043173

'FrameWork > NestJS' 카테고리의 다른 글

[NestJS] Custom decorator  (0) 2023.11.28
[NestJS] Exception filter  (0) 2023.11.28
[NestJS] interceptor  (0) 2023.11.27
[NestJS] Guard  (0) 2023.11.27
[NestJS] Middleware  (0) 2023.11.25