1. 파이프
- 요청이 라우터 핸들러로 전달하기 전에 요청 객체를 변환할 수 있는 기회를 제공
- 미들웨어의 역할과 비슷함.
- 보통 파이프는 2가지 목적으로 사용된다.
- 변환(transformation) : 입력 데이터를 원하는 형식으로 변환
- 유효성 검사(validation) : 입력 데이터가 사용자가 정한 기중에 유효하지 않은 경우 예외 처리
- @nest/common 패키지에는 여러 내장 파이프가 마련되어 있음
- ValidationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe
- 전달된 인수의 타입을 검사하는 용도.
@Get('/:id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
* 만약 id에 정수가 아닌 다른 타입의 값을 넣게 될 경우 유효성 에러가 발생하게 된다.
- 클래스를 전달하지 않고, 파이프 객체를 직접 생성하여 전달할 수도 있다.(객체의 동작을 원하는대로 바꾸고자 할 때 사용한다.)
@Get('/:id')
findOne(
@Param(
'id',
new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }),
)
id: number,
) {
return this.userService.findOne(id);
}
DefaultValuePipe
- 인수의 값에 기본값을 설정할 때 사용한다.
- 쿼리 매개변수가 생략된 경우 유용하게 사용할 수 있다.
@Get()
findAll(
@Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number,
@Query('limit', new DefaultValuePipe(0), ParseIntPipe) limit: number,
) {
console.log(offset, limit);
return this.userService.findAll();
}
커스텀 파이프를 통해 ValidationPipe 직접 만들어 보기
- 커스텀 파이프는 PipeTransform 인터페이스를 상속받은 클래스에 @Injectable 데커레이터를 붙여준다.
@Injectable()
export class ValidationPipe implements PipeTransform {
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
console.log(metadata);
return value;
}
}
- 구현해야 하는 transform 함수는 다음처럼 2개의 매개변수를 가지고 있다.
- value: 현재 파이프에 전돨된 인수
- metadata: 현재 파이프에 전달된 인수의 메타데이터
- ArgumentMetadata의 정의는 다음과 같다.
export interface ArgumentMetadata {
readonly type : Paramtype;
readonly metatype? : Type<any> | undefined;
readonly data?: string | undefined;
}
export declare type Paramtype = 'body' | 'query' | 'param' | 'custom';
- type: 파이프에 전달된 인수가 body, query, param, custom인지 나타냄
- metatype: 라우트 핸들러에 정의도니 인수의 타입
- data : 데커레이터에 전달된 문자열, 즉 매개변수의 이름.
- 예를 들어 유저 정보를 가져오는 라우터 핸들러를 다음과 같이 구현했다고 해보자
@Get('/:id')
findOne(@Param('id', ValidationPipe) id: number) {
return this.userService.findOne(id);
}
- ArgumentMetadata는 라우터 핸들러에서 다음을 의미한다.
2. 유효성 검사 파이프 만들기
- nest 공식 문서에서는 @UsePipes 데커레이터와 joi 라이브러리를 이용하여 커스텀 파이프를 바인딩하는 방법을 설명하고 있다.
- 하지만 우린 class-validator쓸거임
$ yarn add class-validator class-transformer
- 신규 유저 생성시 본문이 유효성에 적합한지 검사해보기
- 유저 생성시 본문을 받아올 dto를 다음과 같이 정의한다.
import { IsString, MinLength, MaxLength, IsEmail } from 'class-validator';
export class CreateUserDto {
@IsString() // 문자열인가?
@MinLength(1) // 문자열 길이가 1을 넘기는가?
@MaxLength(20) // 문자열의 길이가 10 이하인가?
name: string;
@IsEmail() //email 형식을 따르는가?
email: string;
readonly password: string;
}
- 이제 위에서 정의한 것과 같은 dto 객체를 받아서 유효성 검사를 하는 파이프(ValidationPipe)를 직접 구현해 보자.
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
//(1)metatype이 비어있거나, 파이프가 지원하는 타입인지 검사
if (!metatype || !this.toValidate(metatype)) {
return value;
}
//(2)plainToClass를 통해 순수 js 객체를 클래스의 객체로 바꿔줌.
const object = plainToClass(metatype, value);
//(3)유효성 검사
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
//toValdiate 함수 선언
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
- (2)의 경우 네트워크 요청을 통해 들어온 데이터는 역질렬화 과정에서 본문의 객체가 아무런 타임 정보도 가지고 있지 않기 때문에 타입을 지정하는 변환 과정을 plainT0Class로 수행한다.
- 유효성 검사를 통과했다면 원래의 값 그대로 전달한다.
- 이제 이 ValidationPipe를 적용해보자.
//회원가입 로직
@Post()
async createUser(@Body(ValidationPipe) dto: CreateUserDto): Promise<void> {
const { name, email, password //회원가입 로직
@Post()
async createUser(@Body(ValidationPipe) dto: CreateUserDto): Promise<void> {
const { name, email, password } = dto;
await this.userService.createUser(name, email, password);
console.log(dto);
} } = dto;
await this.userService.createUser(name, email, password);
console.log(dto);
}
- 원하는 파이프를 모든 핸들러에서 일일이 지정하지 않고 전역으로 설정하려면 부트스트랩 과정에서 적용하면 된다.
// main.ts
import { ValidationPipe } from './validation.pipe';
async function bootstart() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe())
await app.listen(3000);
}
bootstrap();
- Nest에서 제공하는 ValidationPipe를 전역으로 적용한다. 이때 class-transformer가 적용되게 하려면 transform 속성을 true로 주어야 한다.
// main.ts
import { ValidationPipe } from './validation.pipe';
async function bootstart() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
transform: true,
}));
await app.listen(3000);
}
bootstrap();
class-transformer
- @Transform 데커레이터의 정의를 살펴보자.
- Transform 데커레이터는 transformFn을 인수로 받는다.
- transformFn은 이 데커레이터가 적용되는 속성의 값(value)과 그 속성을 가지고 있는 객체(obj) 등을 인수로 받아 속성을 변형한 후 리턴하는 함수이다.
@Transform((params) => {
console.log(params);
return params.value;
})
@IsString()
@MinLength(1)
@MaxLength(20)
name: string;
- 위 코드는 name속성에 데커레이터를 다음과 같이 적용한 것이다.
- 위 api 요청에 대한 값으로 다음과 같이 출력된다.
- 이는 TransformFnParams이다.
{
value: '홍길동'
key: 'name',
obj: {
name: '홍길동',
email: 'kxxxx@gmail.com',
password: 'pass1232'
},
type:0,
options: {
......
}
- 그래서 다음과 같이 value의 공백을 제거하는 로직으로 사용해도 된다.
@Transform((params) => params.value.trim())
@IsString()
@MinLength(1)
@MaxLength(20)
name: string;
- TransformFnParams에는 obj 속성이 있다. obj는 현재 속성이 있는 객체를 가리킨다.
- 즉, name 속서을 가지고 있는 CreateUserDto 객체를 뜻한다.
- Transformer에서는 이 obj를 활용하여 password에는 name과 동일한 문자열을 포함할 수 없도록 하는 로직을 구현할 수 있다.
@Transform(({ value, obj }) => {
if (obj.password.includes(obj.name.trim())) {
throw new BadRequestException(
'password는 name과 같은 문자열을 포함할 수 없습니다.',
);
}
return value.trim();
})
- Transform은 입력 값을 원하는 값으로 바꾸어주기 위해 사용하는데, 여기 안에서 예외처리까지 일어나게 되면 보기 매우 불편할 수 있다.
- 암튼 불편하다.
- 그런 경우에는 직접 필요한 유효성 검사를 수행하는 데커레이터를 만들어 활용할 수 있다.
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
export function NotIn(property: string, validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return (object: Object, propertyName: string) => {
registerDecorator({
name: 'NotIn',
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [property],
validator: {
validate(value: any, args: ValidationArguments) {
const [relatePropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatePropertyName];
return (
typeof value === 'string' &&
typeof relatedValue === 'string' &&
!relatedValue.includes(value)
);
},
},
});
};
}
'Back-End > Nest.js' 카테고리의 다른 글
예외 처리 (0) | 2023.02.13 |
---|---|
내장 로거 (0) | 2023.02.13 |
동적 모듈을 활용한 환경 변수 구성 (0) | 2023.01.25 |
멋사 2주차 JS decorator, MVC 패턴, Nest.js 찍먹 (0) | 2023.01.11 |
멋사 스터디 2주차 HTTP, RESTFUL,웹 프레임워크 (0) | 2023.01.10 |