새소식

반응형
Back-end/NestJS

[NestJS | Docs] Pipes 알아보기 (feat. 변환과 검증)

2024.02.09
  • -
반응형

1. Pipes란?

Pipe는 Nest에서 PipeTransform 인터페이스를 구현하는 클래스이며 이 클래스 역시 @Injectable() 데코레이터가 붙습니다.

 

 

 

pipe는 두 가지의 전형적인 사용 사례를 갖습니다.

  • transformation(변환): 입력 데이터를 원하는 형태로 변환합니다. (e.g., 문자열에서 정수형 변환)
  • validation(검증): 입력 데이터를 검증하고 만약 통과되면 그 값을 바꾸지 않고 그대로 보내고, 검증에 실패하면 예외를 발생시킵니다.

 

두 경우 모두에서 Pipe는 컨트롤러 라우트 핸들러가 처리 중인 인수를 대상으로 작동합니다.

 

Nest는 메서드(핸들러)가 호출되기 직전에 pipe를 삽입하고, pipe는 메서드로 전달되는 인수를 받아 이를 대상으로 로직을 작동합니다. 이때 모든 변환 또는 유효성 검사 작업이 수행되고, 그 후에 라우트 핸들러가 (잠재적으로) 변환된 인수를 사용하여 호출되는 식입니다.

 

Nest에는 바로 사용할 수 있는 여러 가지 기본 제공 pipe들이 존재합니다. custom pipe를 직접 만들 수도 있는데, 이번 포스팅에서는 기본 제공 pipe에 대한 소개와 이를 라우트 핸들러에 바인딩하는 방법을 먼저 보여드릴 것입니다. 그 다음에 몇 가지 custom pipe를 살펴보고 처음부터 pipe를 구축하는 방법을 보여 드리겠습니다.

 

Pipe와 exception(예외)

pipe는 예외 영역 내에서 실행됩니다. 즉 pipe에서 변환이나 검증에 실패하는 경우에 예외가 발생할 수 있는데 예외가 발생하면 해당 예외가 exception layer(global exception filter 및 현재 context에 적용되는 모든 exception filter)에서 처리됩니다. 위의 내용을 고려할 때, pipe에서 예외가 발생하면 컨트롤러 메서드가 이후에 실행되지 않는다는 점을 분명하게 아실 수 있을 겁니다.

 

이는 시스템 경계(boudary)에 외부 소스에서 애플리케이션으로 들어오는 데이터의 유효성을 검사하는 대표적인 사례 기법을 제공합니다.

 

1-1. Build-in pipes (내장 파이프)

Nest는 다음과 같은 9가지의 바로 사용 가능한 내장 pipe를 제공합니다.

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

 

이들은 모두 @nestjs/common 패키지에서 제공됩니다.

 

ParseIntPipe 사용에 대해서 간단히 살펴보겠습니다. 이는 메서드 핸들러 매개변수가 JavaScript 정수로 변환되도록 하거나 변환에 실패하면 예외를 던지는 Pipe의 변환 사용 사례의 예시입니다.

 

이 장의 뒷부분에서는 ParseIntPipe에 대한 간단한 custom 구현을 보여드리겠습니다. 아래 예제 기법은 다른 기본 제공 변환 파이프에도 적용되는 코드입니다.(Parse* 파이프라고 부르는 것들)

 

1-2. Binding pipes (파이프 바인딩)

이러한 파이프들을 사용하기 위해 파이프 클래스의 인스턴스를 적절한 context에 바인딩해야 합니다. ParseIntPipe 예제에서는 파이프를 특정 라우트 핸들러 메서드와 연결하고 메서드가 호출되기 전에 파이프가 실행되도록 하려고 합니다. 이를 위해 메서드 매개변수 수준에서 파이프를 바인딩하는 구문을 사용합니다.

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

 

이렇게 하면 findOne() 메서드에서 받은 매개 변수가 숫자(this.catsService.findOne() 호출의 예상대로)이거나 라우트 핸들러가 호출되기 전에 예외가 발생하는 두 가지 조건 중 하나가 참인지를 확인할 수 있습니다.

 

예를 들어 다음과 같은 경로로 요청한다고 가정해 보겠습니다.

GET localhost:3000/abc

 

 

Nest는 그러면 다음과 같은 예외를 발생시킬 것입니다.

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

 

예외가 발생하면 findOne() 메서드의 본문이 실행되지 않습니다.

 

위의 예제에서는 인스턴스가 아닌 클래스(ParseIntPipe)를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡김으로써 의존성 주입을 활용하였습니다. pipeguard와 마찬가지로, 대신 in-place 인스턴스를 전달할 수도 있습니다. in-place 인스턴스 전달은 옵션을 전달하여 내장된 파이프의 동작을 커스터마이징 하려는 경우에 유용합니다.

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

 

다른 변환 파이프들을 바인딩하는 것도 비슷하게 작동합니다. 이러한 파이프들은 모두 경로 매개변수(Param), 쿼리 문자열 매개변수(Query) 및 요청 본문 값(Body)의 유효성을 검사하는 context에서 작동합니다.

 

 

Query string parameter(쿼리 문자열 매개변수)를 사용한 예시도 살펴봅시다.

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

 

다음은 string 타입의 매개변수를 파싱하고 해당 매개변수가 UUID인지 확인하는 ParseUUIDPipe의 사용 예시입니다.

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

 

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

 

위에서 다양한 Parse* 내장 파이프 가족을 바인딩하는 예제에 대해서 살펴보았습니다. 유효성 검사(validation) 파이프를 바인딩 하는 것은 조금 다른데 이에 대해서는 다른 포스팅에서 더 자세히 다뤄보도록 하겠습니다.

 

1-2. Custom pipes

앞서 언급했듯이 우리는 custom pipe를 직접 만들 수가 있습니다. Nest는 강력한 기본 제공 ParseIntPipe 및 ValidationPipe를 제공하지만, custom pipe가 어떻게 구성되는지 알아보기 위해 각각의 간단한 custom 버전부터 구축해 보겠습니다.

 

간단한 ValidationPipe로 시작하겠습니다. 처음에는 단순히 입력값을 받고 아무런 작업 없이 즉시 동일한 값을 반환하도록 하여 동일 함수처럼 동작하게 하는 예제입니다.

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

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

 

PipeTransform<T, R>은 모든 파이프에서 구현해야 하는 일반 인터페이스입니다. 이 일반 인터페이스는 T를 사용하여 입력 값을 타입을 나타내고, R을 사용하여 transform() 메서드의 반환 타입을 나타냅니다.

 

모든 파이프들은 이러한 PipeTransform 인터페이스의 약속을 지키기 위해 transform() 메서드를 구현해야 합니다.

 

이 메서드에는 두 개의 매개변수가 있습니다:

  • value
  • metadata

value 매개변수는 라우트 핸들러 메서드에게 넘기기 전에 현재 처리되고 있는 메서드 인자이며, metadata는 현재 처리되는 중인 메서드 인자의 메타데이터를 나타냅니다.

 

metatdata 객체에는 다음과 같은 속성들이 있습니다.

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

 

이러한 속성들은 현재 처리 중인 인자를 표현합니다.

 

 

  • type
    • 인수가 본문 @Body(), 쿼리 @Query(), 매개변수 @Param() 또는 custom 매개변수인지 여부를 나타냅니다.
  • metatype
    • 인수의 메타 타입을 제공합니다.(예: String)
    • 참고: 라우트 핸들러 메서드 시그니처에서 타입 선언을 생략하거나 vanilla JavaScript를 사용하는 경우 이 값은 정의되지 않습니다.
  • data
    • 데코레이터에 전달된 문자열(예: @Body('string')) 입니다. 데코레이터 괄호를 비워두면 정의되지 않습니다.

 

주의: TypeScript 인터페이스는 트랜스파일링 중에 사라지기 때문에 메서드 매개변수의 타입이 클래스 대신 인터페이스로 선언된 경우 메타타입은 Object가 됩니다.

 

1-3. Schema based validation (스키마 기반 검증)

유효성 검사 파이프를 좀 더 유용하게 만들어 봅시다. Service 메서드를 실행하기 전에 post body 객체가 유효한지 확인해야 하는 CatsController의 create() 메서드를 자세히 살펴봅시다.

 

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

 

CreateCatDto body 매개변수에 집중해 봅시다. 이 매개변수 타입은 CreateCatDto 입니다.

 

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

 

create() 메서드로 들어오는 모든 요청에 유효한 body를 포함하는지 확인해야 합니다. 그렇기 때문에 createCatDto 객체의 3개의 멤버에 대한 유효성을 검사해야 합니다.

 

라우트 핸들러 메서드 내부에서 이 작업을 수행할 수도 있지만 이는 단일 책임 원칙(SRP)을 위반하므로 이상적이지 않은 방법입니다.

 

또 다른 접근 방식은 validator 클래스를 따로 생성하고 그곳에 작업을 위임하는 것입니다. 다만, 이 방법은 각 메서드를 시작할 때마다 이 validator 호출을 기억해야 한다는 단점이 있습니다.

 

그러면 미들웨어로 validator를 만드는 것은 어떨까요? 이 방법은 잘 작동 하긴 하겠지만, 안타깝게도 전체 애플리케이션의 모든 context에서 사용할 수 있는 범용 미들웨어를 만드는 것은 불가능합니다. 왜냐하면 미들웨어는 이후에 호출될 핸들러에 대한 정보와 그것의 매개변수 등 execution context(실행 컨텍스트)를 인식하지 못하기 때문입니다.

 

이래서 우리는 파이프를 설계해야 하는 것입니다.

 

이제는 validator 파이프를 구체화해 보도록 하겠습니다.

 

1-4. Object schema validation (객체 스키마 검증)

깔끔하고 반복 없는 방식으로 객체 유효성 검사를 수행하는 데 사용할 수 있는 몇 가지 접근 방식이 존재합니다.

 

일반적인 접근 방식 중 하나는, 스키마 기반의 유효성 검사를 사용하는 것입니다.

 

이 접근 방식을 시도해봅시다.

 

Zod 라이브러리를 사용하면 읽기 쉬운 API를 사용하여 간단한 방법으로 스키마를 만들 수 있습니다. Zod 기반 스키마를 사용하는 validator 파이프를 구축해 보겠습니다.

 

먼저 필요한 패키지인 zod를 설치합니다.

$ npm install --save zod

 

아래 코드 샘플에서는 스키마생성자의 인수로 사용하는 간단한 클래스를 보여줍니다. 그런 다음 제공된 스키마에 대해 요청 인수의 유효성을 검사하는 schema.parse() 메서드를 적용합니다.

 

앞서 언급했듯이 유효성 검사 파이프는 값을 변경하지 않고 반환하거나 예외를 던집니다.

 

다음 섹션에서는 @UsePipes() 데코레이터를 사용하여 주어진 컨트롤러 메서드에 적절한 스키마를 제공하는 방법을 살펴보겠습니다. 이렇게 하면 의도한 대로 여러 context에서 유효성 검사 파이프를 재사용할 수 있습니다.

 

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

 

 

1-5. Binding validation pieps

앞서 우리는 변환(tranformation) 파이프를 어떻게 바인딩하는지에 대해서 보았었습니다. (ParseIntPipe를 비롯한 나머지 Parse* 파이프들)

 

검증(validation) 파이프를 바인딩하는 것 또한 굉장히 쉽습니다.

 

이 경우에 메서드 호출 단계에서 파이프를 바인딩하고 싶다고 해봅시다. 현재 예제에서는 ZodValidationPipe를 사용하려면 다음과 같은 단계를 수행해야 합니다.

  1. ZodValidationPipe의 인스턴스를 생성합니다.
  2. 파이프의 클래스 생성자에서 context 별 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);
}

 

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

 

 

1-6.  Class validator

유효성 검사 기법의 다른 구현 방법을 알아보도록 하겠습니다.

 

Nest는 class-validator 라이브러리와 굉장히 잘 동작합니다. 이 강력한 라이브러리를 사용하면 데코레이터 기반 유효성 검사를 사용할 수 있습니다. 특히 데코레이터 기반 유효성 검사는 처리되는 속성의 메타타입에 접근할 수 있기 때문에 Nest의 파이프 기능과 결합 시에 매우 강력합니다. 

 

시작 전에 필요한 패키지를 설치합니다.

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

 

설치가 완료되면 CreateCatDto 클래스에 몇 가지 데코레이터를 추가할 수 있습니다. 이 기법의 중요한 장점은 별도의 유효성 검사 클래스를 만들지 않고도 CreateCatDto 클래스가 Post body 객체에 대해 하나의 source로 유지된다는 것입니다.

 

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

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

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

 

이제 이러한 annotation을 사용하는 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);
  }
}

 

다시 한 번 말씀드리자면, ValidationPipe는 Nest에서 기본으로 제공되기 때문에 일반적인 유효성 검사 파이프를 직접 구축할 필요가 없습니다. 기본 제공 ValdationPipe는 이 장에서 빌드한 샘플보다 더 많은 옵션을 제공하지만, 커스텀 구축 파이프의 메커니즘을 설명하기 위해 default 값으로 설정하였습니다. 여기에서 많은 예제와 함께 자세한 내용을 확인할 수 있습니다.

 

NOTICE
위에서 사용한 class-trasformer 라이브러리는 class-validator 라이브러리 제작자가 같은 사람들이기 때문에 결과적으로 두 라이브러리를 같이 사용해도 매우 잘 어울립니다.

 

그럼 위 코드를 한 번 살펴봅시다. 먼저 trasform() 메서드가 비동기로 표시되어 있다는 점에 주목해야 합니다. 이는 Nest가 동기 및 비동기 파이프를 모두 지원하기 때문에 가능한 것입니다. 이 메서드를 비동기로 설정하는 이유는 class-validator 유효성 검사 중 일부가 비동기식 로직이 포함될 수 있기 때문입니다.(Promise 활용)

 

다음으로 metatype 필드를 metatype 매개변수로 추출하기 위해 desctructuring(비구조화) 할당을 사용하고 있다는 점에 유의해야 합니다.(ArgumentMetadata에서 metatype만 추출) 이것은 전체 ArgumentMetadata를 가져온 다음에 metatype 변수에 할당하기 위한 추가 구문을 만드는 것을 줄여 표현 하는 방식이라고 볼 수 있습니다.

  • 즉, 원하는 것을 바로 가져오는 구문입니다.

 

다음으로, helper 함수인 toValidate() 메서드를 봅시다. 이 함수는 현재 처리 중인 인수가 네이티브 JavaScript 타입일 때 유효성 검사 단계를 우회(skip)하는 역할을 합니다. (이 타입들에는 유효성 검사 데코레이터가 붙여질 수 없기 때문에 유효성 검사 단계를 거칠 이유가 없습니다.)

  • 유효성 검사 데코레이터는 DTO 클래스의 속성에 붙여지는 것으로 네이티브 타입에 대해서는 앞선 ParseIntPipe와 같이 내장 파이프를 이용하여 검증 및 변환을 진행합니다.

 

그 다음은 유효성 검사를 적용할 수 있도록 class-transformer 함수인 plainToInstance를 사용하여 일반 JavaScript 인수 객체를 타입이 지정된 객체로 변환합니다. 이 작업을 수행해야 하는 이유는 네트워크 요청에서 역직렬화된 수신 post body에는 타입에 대한 정보가 없기 때문입니다(Express와 같은 기본 플랫폼이 작동하는 방식).

 

class-validator는 앞서 DTO에 대해 정의한 유효성 검사 데코레이터를 사용해야 하므로, 들어오는 body를 단순한 vanilla 객체가 아니라 적절하게 데코레이션된 객체로 처리하려면 이러한 변환을 수행해야 하는 것입니다.

 

마지막으로, 앞서 언급했듯이 유효성 검사 파이프이므로 그러한 값을 변경하지 않고 반환하거나 예외를 던지는 작업을 추가합니다.

 

마지막 단계는 ValidationPipe를 바인딩하는 것입니다. 파이프는 매개변수 범위, 메서드 범위, 컨트롤러 범위 또는 전역 범위가 될 수 있습니다. 앞서 Zod 기반 유효성 검사 파이프를 사용하여 메서드 수준에서 파이프를 바인딩하는 예제를 살펴봤습니다.

 

아래 예제에서는 파이프 인스턴스를 라우트 핸들러 @Body() 데코레이터에 바인딩하여 파이프가 호출되어 post body 유효성을 검사하도록 하였습니다.

 

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

 

매개변수 범위 지정 파이프는 유효성 검사 로직이 지정된 매개변수 하나에만 신경쓸 때 유용합니다.

 

1-7. Global scoped pipes (전역 범위 파이프)

ValidationPipe는 최대한 범용적으로 사용되도록 만들어졌기 때문에 전체 애플리케이션의 모든 라우트 핸들러에 적용되도록 전역 범위의 파이프로 설정할때 그 유용성을 최대한 발휘할 수 있습니다.

 

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

 

NOTICE
하이브리드 앱의 경우 UseGlobalPipes() 메서드는 gateway 및 microservice에 대한 파이프를 설정하지 않습니다. "표준"(non-hybrid) microservice 앱의 경우 userGlobalPipes()는 파이프를 전역적으로 마운트합니다.

 

전역 파이프는 모든 컨트롤러와 모든 라우트 핸들러에 대해 전체 애플리케이션에 대해 적용됩니다. 

 

의존성 주입과 관련하여 모듈 외부(main.ts)에서 등록한 글로벌 파이프는 바인딩이 모듈의 context 외부에서 수행되기 때문에 의존성 주입을 할 수 없습니다. 이 문제를 해결하기 위해선 구성을 아래과 같이 하여 모든 모듈에서 직접 전역 파이프를 설정해야 합니다.

 

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

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

 

이 접근 방식을 사용하여 파이프에 대한 의존성 주입을 수행할 때는 이 구조가 사용되는 모듈에 관계없이 파이프가 실제로는 전역이라는 점에 유의해야 합니다.

 

이 작업을 어디에서 수행해야 할까요? 파이프가 정의된 모듈(위 예시에서는 ValidationPipe)을 선택해야 합니다.

 

또한 useClass 만이 custom provider를 처리할 수 있는 유일한 방법은 아니라는 점도 유의합니다.(useExisting, useFactory,...)

 

1-8. The bulit-in ValidationPipe

다시 한 번 말씀드리자면, ValidationPipe는 Nest에서 기본으로 제공되기 때문에 일반적인 유효성 검사 파이프를 직접 만들 필요가 없습니다. 기본 제공 ValidationPipe는 이 장에서 빌드한 샘플보다 더 많은 옵션을 제공하지만, custom 구축 파이프의 메커니즘을 설명하기 위해 가장 기본으로 유지한 것입니다. 여기에서 많은 예제와 함께 자세한 내용을 확인할 수 있습니다.

 

1-9. Transformation use case (변환 사용 사례)

유효성 검사만이 custom 파이프의 유일한 사용 사례는 아닙니다. 이 장의 서두에서 파이프를 사용하여 입력 데이터를 원하는 형식으로 변환할 수도 있다고 언급했었습니다. 이는 transform() 함수에서 반환된 값이 인수의 이전 값을 완전히 재정의하기 때문에 가능합니다.

 

그렇다면 이는 언제 유용한 것일까요?

 

클라이언트로부터 전달된 데이터가 라우트 핸들러 메서드에서 제대로 처리되기 전에 문자열을 정수로 변환하는 등과 같이 일부 변경을 거쳐야 하는 경우가 있습니다. 또한 일부 필수 데이터 필드가 누락되었을 때 기본값을 적용하고자 할 수도 있습니다.

 

변환 파이프는 클라이언트 요청과 요청 핸들러 사이에 처리 함수를 삽입하여 이러한 기능을 수행할 수 있습니다.

 

다음은 문자열을 정수 값으로 파싱하는 간단한 ParseIntPipe입니다. 앞에서 언급했듯이 Nest에는 더 정교한 ParseIntPipe가 이미 내장되어 있습니다. 여기서는 해당 파이프의 간단한 구현을 보여주기 위해 custom 변환 파이프의 예로 사용한 것입니다.

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

 

 

그런 다음 우리는 이 파이프를 선택된 param에 바인딩할 수 있습니다.

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

 

또 다른 유용한 변환 사례는 요청에서 제공된 id를 사용하여 데이터베이스에서 기존 user 엔티티를 반환하도록 하는 것입니다.

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

 

import { PipeTransform, Injectable, NotFoundException, ArgumentMetadata } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from './user.entity'; // UserEntity 경로에 맞게 수정
import { Repository } from 'typeorm';

@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {}

  async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
    const id = parseInt(value, 10); // 요청에서 받은 ID 값을 정수로 변환
    const user = await this.userRepository.findOne(id);

    if (!user) {
      // 사용자를 찾을 수 없는 경우 예외 발생
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user; // 찾은 사용자 엔티티 반환
  }
}

 

다른 모든 변환 파이프와 마찬가지로 입력 값(Id)를 받고 출력 값(UserEntity 객체)을 반환한다는 점에 유의해야 합니다. 이렇게 하면 bolierplate 코드를 핸들러에서 일반 파이프로 추상화기 때문에 코드를 보다 선언적이고 간결하게 만들 수 있습니다.

 

1-10. Providing defaults (기본값 제공)

Parse* 파이프는 매개변수 값이 정의되어 있기를 기대하고 있습니다. 그렇기에 null 또는 undefined 값을 받으면 예외가 발생합니다. 엔드포인트에서 누락된 쿼리 문자열 매개변수 값을 처리할 수 있도록 하려면 Parse* 파이프가 이러한 값에 대해 작동하기 전에 주입할 기본값을 제공해야 합니다. DefaultValiePipe가 바로 그 역할을 합니다.

 

아래와 같이 관련 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 });
}

 

 

이렇게 Pipe에 대해서 알아보았습니다. Pipe는 validation과 transformation을 위해 사용되는 것으로 class-validator와 class-transfomer와 잘 어울려 동작합니다. 이에 대해서는 추후에 올릴 포스팅을 보시면 더욱 도움이 되실 것 같습니다.

 

감사합니다.

 

 

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.