파이프는 PipeTransform 인터페이스를 구현한 @Injectable() 데코레이터가 달린 클래스다.
파이프에는 두 가지 일반적인 사용 사례가 있다.
- 변환 : 입력 데이터를 원하는 형식으로 변환한다(예: 문자열에서 정수로).
- 유효성 검사(validation) : 입력 데이터를 평가하고 유효한 경우 변경하지 않고 그대로 전달. 그렇지 않으면 예외를 발생시킨다.
두 경우 모두 파이프는 컨트롤러 라우트 핸들러에 의해 arguments를 처리하는 작업을 수행한다 . Nest는 메소드가 호출되기 직전에 파이프를 삽입하고 파이프는 메소드에 대한 인수를 수신하여 작동하게된다. 모든 변환 또는 유효성 검사 작업은 해당 시점에 수행되며, 그 후에는 (잠재적으로) 변환된 인수를 사용하여 라우트 핸들러가 호출된다.
Nest에는 기본적으로 사용할 수 있는 다양한 파이프가 내장되어 있고, 자신만의 맞춤형 파이프를 만들 수도 있다. 이 장에서는 내장 파이프를 소개하고 이를 경로 처리기에 바인딩하는 방법을 보여준다. 그런 다음 몇 가지 맞춤형 파이프를 검사하여 처음부터 파이프를 만드는 방법을 구현해보자.
힌트) 파이프는 예외 구역 내부에서 실행된다. 이는 파이프가 예외를 발생시킬 때 예외 계층(전역 예외 필터 및 현재 컨텍스트에 적용되는 모든 예외 필터 )에 의해 처리된다는 것을 의미한다. 위의 내용을 고려하면 파이프에서 예외가 발생하면 이후에 컨트롤러 메서드가 실행되지 않는다는 점을 분명히 알아야한다. 이는 시스템 경계의 외부 소스에서 애플리케이션으로 들어오는 데이터의 유효성을 검사하기 위한 좋은 사례를 제공한다.
내장 파이프
Nest에는 기본적으로 사용 가능한 9개의 파이프가 함께 제공됩니다.
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
@nestjs/common. 패키지에 포함되어 있다.
ParseIntPipe 의 사용법을 간단히 살펴보자 . 이는 파이프가 메소드 핸들러 매개변수가 JavaScript 정수로 변환되도록 보장하는(또는 변환이 실패할 경우 예외를 발생시키는) 예입니다 . 이 장의 뒷부분에서는 간단한 ParseIntPipe를 위한 간단한 구현을 시도해 보자. 아래 코드의 예는 다른 내장 변환 파이프( 이 장에서는 Parse* 파이프 라고 지칭할 ParseBoolPipe, ParseFloatPipe, ParseEnum, PipeParseArray, PipeParseUUIDPipe)에도 적용된다.
바인딩 파이프
파이프를 사용하려면 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인딩해야 한다. 이 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 )를 전달하여 인스턴스화에 대한 책임을 프레임워크에 맡기고 종속성 주입을 활성화된다. 파이프 및 가드와 마찬가지로 대신 내부 인스턴스를 전달할 수 있다. 옵션을 전달하여 내장 파이프의 동작을 사용자 정의하려는 경우 내부 인스턴스를 전달하는 것이 유용하다.
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
다른 변환 파이프(모든 Parse* 파이프) 바인딩도 비슷하게 작동한다. 이러한 파이프는 모두 경로 매개변수, 쿼리 문자열 매개변수 및 요청 본문 값의 유효성을 검사하는 맥락에서 작동한다.
예를 들어 쿼리 문자열 매개변수를 사용하면 다음과 같다.
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
다음 ParseUUIDPipe 은 문자열 매개변수를 구문 분석하고 UUID인지 확인하기 위해 사용하는 예이다 .
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
힌트) ParseUUIDPipe() 를 사용하여, 버전 3, 4 또는 5에서 UUID를 구문 분석 하는 경우 또는 특정 버전의 UUID만 필요한 경우 파이프 옵션에 버전을 전달할 수 있다.
위에서 우리는 다양한 Parse*종류의 내장 파이프를 바인딩하는 예를 보았다. 유효성 검사 파이프 바인딩은 약간 다르다. 이에 대해서는 다음 섹션에서 논의하겠다.
맞춤형 파이프
언급한 대로 사용자 정의 파이프를 직접 만들 수 있다. Nest는 강력한 내장 ParseIntPipe및 ValidationPipe 을 제공하지만 처음부터 각각의 간단한 사용자 정의 버전을 만들어 사용자 정의 파이프가 어떻게 구성되는지 살펴보자.
간단한 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는 value 입력 유형을 나타내고 transform() 메소드 R의 반환 유형을 나타내는데 사용된다.
PipeTransform 을 구현하는 모든 파이프는 인터페이스 함수 transform() 메서드를 구현해야 한다 . 이 메서드에는 두 가지 매개변수가 있습니다.
- value
- metadata
alue 변수는 현재 처리된 메서드 인수(경로 처리 메서드에서 수신되기 전)이며 metadata는 현재 처리된 메서드 인수의 메타데이터 이고, 메타데이터 개체에는 다음과 같은 속성이 있다.
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
이러한 속성은 현재 처리된 인수를 설명한다.
type | 인수가 body @Body(), query @Query(), param @Param()또는 맞춤 매개변수인지 여부를 나타낸다. |
metatype | 인수의 메타 유형을 제공한다, 예를 들어 String 타입으로 지정하고, 값이 undefined 라면, 라우트 핸들러 메서드에서 유형 선언을 생략하거나 바닐라 JavaScript를 사용하는 경우일 것이다. |
data | 데코레이터에 전달된 문자열이다. 예를 들어 @Body('string')처럼 쓸 수 있다. undefined 로 허용하려면, 데코레이터 괄호를 비워두면 됩니다. |
주의) TypeScript 인터페이스는 변환 중에 사라집니다. 따라서 메소드 매개변수의 유형이 클래스 대신 인터페이스로 선언되면 metatype 값 은 Object 가 됩니다.
스키마 기반 검증
검증 파이프를 좀 더 유용하게 만들어 보자. 서비스 메서드를 실행하기 전에 게시물 본문 개체가 유효한지 확인하고 싶은 CatsController 의 create()메서드를 자세히 살펴보자.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
body 매개변수 createCatDto 에 주의깊게보자, 타입 CreateCatDto은 다음과 같다.
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
우리는 create 메소드로 들어오는 모든 요청에 유효한 본문이 포함되어 있는지 확인하려한다. 따라서 우리는 객체의 세 멤버를 검증해야 한다 createCatDto. 라우트 핸들러 메서드 내에서 이 작업을 수행할 수 있지만 그렇게 하면 단일 책임 원칙 (SRP) 을 위반하므로 이상적이지 않다 .
또 다른 접근 방식은 Validator 클래스를 만들고 거기에 작업을 위임하는 것이다 . 이는 각 메서드 시작 시 이 유효성 검사기를 호출하는 것을 기억해야 한다는 단점이 있다.
유효성 검사 미들웨어를 만드는 것은 어떻습니까? 가능하지만, 불행하게도 전체 애플리케이션의 모든 컨텍스트에서 사용할 수 있는 일반 미들웨어를 만드는 것은 불가능합니다 . 이는 미들웨어가 호출될 핸들러와 해당 매개변수를 포함하여 실행 컨텍스트를 인식하지 못하기 때문이다.
물론 이것은 파이프가 설계된 사용 사례와 정확히 같다. 이제 검증 파이프를 개선해 보자.
객체 스키마 검증
DRY 원칙에 의거한 개체 유효성 검사를 수행하는 데 사용할 수 있는 여러가지 접근 방식이 있습니다. 일반적인 접근 방식 중 하나는 스키마 기반 유효성 검사를 사용하는 것이다.
Zod 라이브러리를 사용하면 읽기 가능한 API를 사용하여 간단한 방법으로 스키마를 생성할 수 있다. Zod 기반 스키마를 활용하는 유효성 검사 파이프를 구축해 보자.
우선, 패키지 설치가 필요하다.
$ npm install --save zod
아래 코드 샘플에서는 스키마를 생성자 인수로 사용하는 간단한 클래스를 만든다. 그런 다음, 들어오는 인수의 유효성을 검사하는 schema.parse() 메서드를 적용한다 .
앞서 언급했듯이 유효성 검사 파이프는 변경되지 않은 값을 반환하거나 예외를 발생시킨다.
다음 섹션에서는 @UsePipes() 데코레이터를 사용하여 특정 컨트롤러 메서드에 적절한 스키마를 제공하는 방법을 살펴보자. 그렇게 하면 우리가 설정한 대로 유효성 검사 파이프를 컨텍스트 전체에서 재사용할 수 있다.
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 {
this.schema.parse(value);
} catch (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
바인딩 검증 파이프
앞서 우리는 변환 파이프( ParseIntPipe 또는 Parse* 파이프와 유사 ) 를 바인딩하는 방법을 살펴보았다 .
유효성 검사 파이프를 바인딩하는 것도 매우 간단하다.
이 경우에는 메서드 호출 수준에서 파이프를 바인딩하려고 합니다. 현재 예에서는 ZodValidationPipe 사용하여 다음을 수행해야 한다.
- ZodValidationPipe 인스턴스를 생성.
- 파이프의 클래스 생성자에 컨텍스트별 Zod 스키마를 전달.
- 파이프를 메서드에 바인딩
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() 데코레이터를 사용하여 이를 수행한다.
// cats.controller.ts
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
힌트) @UsePipes() 데코레이터는 @nestjs/common 패키지에서 가져온다.
주의) zod라이브러리를 사용하려면 tsconfig.json 파일에서 strictNullChecks 구성을 활성화 해야 합니다.
클래스 유효성 검사기
주의) 이 섹션에는 TypeScript가 필요하며 앱이 바닐라 JavaScript를 사용하여 작성된 경우에는 사용할 수 없다.
검증 기술의 대체 구현을 살펴보자.
Nest는 클래스 유효성 검사기 라이브러리와 잘 작동한다. 이 강력한 라이브러리를 사용하면 데코레이터 기반 유효성 검사를 사용할 수 있다. 데코레이터 기반 유효성 검사는 처리된 속성의 metatype 에 액세스할 수 있으므로 Nest의 파이프 기능 과 결합할 때 특히 강력하다 . 시작하기 전에 필수 패키지를 설치해야 한다.
$ npm i --save class-validator class-transformer
이것들이 설치되면 CreateCatDto클래스에 데코레이터 몇 개를 추가할 수 있다. 여기서는 이 기술의 중요한 이점을 볼 수 있다. CreateCatDto클래스는 별도의 유효성 검사 클래스를 생성할 필요 없이 Post 본문 개체에 대한 단일 정보 소스로 유지된다.
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
이제 이러한 주석을 사용하는 ValidationPipe 클래스를 만들 수 있습니다 .
// validation.pipe.ts
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 샘플보다 더 많은 옵션을 제공합니다. 이 샘플은 맞춤형 파이프의 메커니즘을 설명하기 위해 기본으로 작성되었다.
이 코드를 살펴보자. 먼저 transform()메소드가 async 로 표시되어 있습니다. 이는 Nest가 동기 파이프와 비동기 파이프를 모두 지원하기 때문에 가능하다. 유효성 검사 중 일부가 비동기식 (프라미스 활용) 일 수 있기 때문에 이 방법으로 만든다 .
다음으로 우리는 metatype 매개변수로 메타타입 필드( ArgumentMetadata 에서 이 멤버만 추출 ) 를 추출하기 위해 구조 분해를 사용하고 있다는 점에 유의하자. 이는 ArgumentMetadata 전체 내용을 가져온 다음 메타 유형 변수를 할당하기 위한 추가 명령문을 갖는 간단한 방법이다 .
다음으로 toValidate() 헬퍼 함수에 주목하자. 처리 중인 현재 인수가 네이티브 JavaScript 유형인 경우 유효성 검사 단계를 우회하는 일을 담당한다(이러한 유효성 검사 데코레이터는 첨부될 수 없으므로 유효성 검사 단계를 통해 실행할 이유가 없다).
다음으로, 유효성 검사를 적용할 수 있도록 클래스 변환기 함수 plainToInstance() 를 사용하여 일반 JavaScript 인수 개체를 형식화된 개체로 변환한다. 이렇게 해야 하는 이유는 네트워크 요청에서 역직렬화될 때 들어오는 게시물 본문 개체에 유형 정보가 없기 때문이다(이것이 Express와 같은 기본 플랫폼이 작동하는 방식이다). 클래스 유효성 검사기는 이전에 DTO에 대해 정의한 유효성 검사 데코레이터를 사용해야 하므로 들어오는 본문을 일반 바닐라 개체가 아닌 적절하게 장식된 개체로 처리하기 위해 이 변환을 수행해야한다.
마지막으로, 앞서 언급한 은 유효성 검사 파이프 이므로 변경되지 않은 값을 반환하거나 예외를 발생시킨다.
마지막 단계는 ValidationPipe 의 바인딩, 파이프는 매개변수 범위, 메서드 범위, 컨트롤러 범위 또는 전역 범위일 수 있다. 앞서 Joi 기반 유효성 검사 파이프를 사용하여 메서드 수준에서 파이프를 바인딩하는 예를 보았다. 아래 예에서는 파이프 인스턴스를 경로 핸들러 @Body()데코레이터에 바인딩하여 파이프가 호출되어 게시물 본문의 유효성을 검사한 예이다.
//cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
매개변수 범위 파이프는 유효성 검사 논리가 지정된 하나의 매개변수에만 관련되는 경우 유용하다.
전역 범위 파이프
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
주의) 하이브리드 앱 의 경우 이 useGlobalPipes( )메서드는 게이트웨이 및 마이크로 서비스에 대한 파이프를 설정하지 않는다. "표준"(비하이브리드) 마이크로서비스 앱의 경우 useGlobalPipes()전역적으로 파이프를 마운트한다.
전역 파이프는 모든 컨트롤러와 모든 경로 처리기에 대해 전체 응용 프로그램에서 사용된다.
종속성 주입 측면에서, 모듈 외부에서 등록된 전역 파이프( 위의 예와 같이 useGlobalPipes() )는 바인딩이 모듈의 컨텍스트 외부에서 수행되었기 때문에 종속성을 주입할 수 없다. 이 문제를 해결하려면 다음 구성을 사용하여 모든 모듈에서 직접 전역 파이프를 설정할 수 있다.
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
힌트) 이 접근 방식을 사용하여 파이프에 대한 종속성 주입을 수행하는 경우 이 구성이 사용되는 모듈에 관계없이 파이프는 실제로 전역적이라는 점에 유의하자. 이 작업은 어디서 수행될까? 바로 파이프( 위 예에서 ValidationPipe )가 정의된 모듈을 선택된다. 또한 useClass 를 사용하는 방법이 사용자 지정 공급자 등록을 처리하는 유일한 방법은 아니다.
내장 검증 파이프
참고로 ValidationPipe 와 같은 일반 검증 파이프는 Nest에서 기본적으로 제공되므로 직접 구축할 필요가 없다 . 내장된 ValidationPipe는 이 장에서 만든 샘플보다 더 많은 옵션을 제공한다.
변환(Trasnformation) 사용 사례
검증은 커스텀 파이프의 유일한 사용 사례가 아니다. 이 장의 시작 부분에서 우리는 파이프가 입력 데이터를 원하는 형식으로 변환 할 수도 있다고 언급했다. 이는 함수에서 반환된 값이 transform인수의 이전 값을 완전히 재정의하기 때문에 가능하다.
이게 언제 언제 유용할까? 때로는 클라이언트에서 전달된 데이터가 라우트 핸들러 메서드에서 적절하게 처리되기 전에 일부 변경(예: 문자열을 정수로 변환)을 거쳐야 한다는 점을 고려해야한다. 또한 일부 필수 데이터 필드가 누락되었을 수 있으므로 기본값을 적용하고 싶을 때가 있다. 변환 파이프는 클라이언트 요청과 요청 핸들러 사이에 처리 기능을 삽입하여 이러한 기능을 수행할 수 있다.
다음 ParseIntPipe 은 문자열을 정수 값으로 구문 분석하는 일을 담당하는 간단한 것이다. (위에서 언급한 것처럼 Nest에는 더욱 정교한 ParseIntPipe 기능이 내장되어 있다. 다만, 이해를 위해서 사용자 정의 변환 파이프의 간단한 예로 포함했다.)
// parse-int.pipe.ts
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);
}
또 다른 유용한 변환 사례는 요청에 제공된 ID를 사용하여 데이터베이스에서 기존 사용자 엔터티를 선택하는 것이다 .
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
이 파이프의 구현은 독자에게 맡기지만 다른 모든 변환 파이프와 마찬가지로 이 파이프도 입력 값(id)을 받고 출력 값( UserEntity 객체) 을 반환한다. 이렇게 하면 핸들러에서 상용구 코드를 추상화하여 공통 파이프로 코드를 더욱 선언적이고 DRY 하게 만들 수 있다.
기본 값 제공
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 });
}
'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글
9. Interceptors (0) | 2023.11.19 |
---|---|
8. Guards (0) | 2023.11.19 |
6. Exception filters (0) | 2023.11.17 |
5. Middleware (1) | 2023.11.16 |
4. Modules (1) | 2023.11.16 |