본문 바로가기

Backend/NestJS

[NestJS] User API 만들기(3) (파이프 Pipe 유효성 검사)

728x90
NestJS 파이프에 대한 공부 기록이다.

개요

파이프는 요청이 라우터 핸들러로 전달되기 전 요청 객체를 변환할 수 있는 기회를 제공한다.

 

파이프는 일반적으로 아래 두 가지의 목적으로 사용한다.

  1. 변환 : 입력 데이터를 원하는 형식으로 변환.
  2. 유효성 검사 : 입력 데이터가 유효한지 검사하고, 그렇지 않다면 예외 처리
라우트 핸들러
엔드포인트마다 동작을 수행하는 컴포넌트
요청 경로와 컨트롤러를 매핑해준다.

변환

파이프를 이용해 입력 데이터를 변환해 보자.

ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe를 통해

전달된 인자의 타입을 파싱 할 수 있다.

 

DefaultValuePipe는 전달된 인자의 값에 기본값을 설정할 때 사용한다.

파이프를 사용해 보자.

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    console.log(id);
  }

위처럼 ParseIntPipe를 사용하면,

id를 문자열에서 정수로 파싱 해서 전달한다.

  @Get()
  getHello(
    @Query('offset', new DefaultValuePipe(10), ParseIntPipe) offset: number,
  ) {
    console.log(offset);
  }

위처럼 기본값을 설정해 주면,

쿼리를 생략해도 offset의 값을 10으로 설정한다.

유효성 검사

파이프 내부 구현

ValidationPipe를 직접 만들면서 내부 구현이 어떻게 되어있는지 이해해 보자.

이해하면 커스텀 파이프가 필요할 때 어떻게 만들면 될지 알 수 있다.

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

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

커스텀 파이프는 PipeTransform 인터페이스를 상속받은 클래스Injectable 데커레이터를 붙여주면 된다.

PipeTransform 인터페이스를 보자.

export interface PipeTransform<T = any, R = any> {
    transform(value: T, metadata: ArgumentMetadata): R;
}

PipeTransform은 위와 같이 생겼고,

두 개의 매개변수를 가진다.

  1. value : 현재 파이프에 전달된 인자
  2. metadata : 현재 파이프에 전달된 인자의 메타데이터

metadata의 타입인 ArgumentMetadata의 인터페이스를 보자.

export interface ArgumentMetadata {
    readonly type: Paramtype;
    readonly metatype?: Type<any> | undefined;
    readonly data?: string | undefined;
}
  1. type : 파이프에 전달된 인수가 본문인지, 쿼리 매개변수인지, 경로 매개변수인지를 나타낸다.
  2. metatype : 라우터 핸들러에 정의된 인수의 타입을 알려준다.
  3. data : 매개변수의 이름을 나타낸다.

위에서 만든 코드를 이용해 metadata를 직접 콘솔에 찍어보자.

  @Get(':id')
  findOne(@Param('id', ValidationPipe) id: number) {
    console.log(id);
  }

위와 같이 만든 파이프를 바인딩하고,

콘솔을 보면

{ metatype: [Function: Number], type: 'param', data: 'id' }

위와 같은 결괏값을 볼 수 있다.

ValidationPipe

DTO의 유효성을 검사해 주는 ValidationPipe를 직접 구현해 보자.

import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';

@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    console.log('metatype : ', metatype);
    console.log('Value : ', value);
    const object = plainToClass(metatype, value);
    console.log('changedValue : ', object);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Failed');
    }
    return value;
  }
  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

중간 과정을 보기 위해 console.log를 넣었다.

 

먼저 transform 메서드가 async로 선언되어 있다.

class-validator의 유효성 검사 메서드가 비동기로 작동할 수 있기 때문이다.

 

첫 번째로 toValidate() 메서드이다.

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }

이 메서드는 처리 중인 인수의 타입이 기본 JS 유형인지 반환하는 메서드이다.

만약 기본 JS 유형이라면 유효성 검사 데커레이터를 연결할 수 없으므로

유효성 검사 단계를 실행할 필요가 없기 때문이다.

 

두 번째는 일반 JS 인수 객체를 타입이 지정된 객체로 변환하는 과정이다.

const object = plainToClass(metatype, value);

이 과정은 타입이 지정되지 않은 요청 객체에 대해 유효성 검사 데커레이터를 사용할 수 있도록

DTO에서 타입이 지정된 객체로 변환해 주는 과정이다.

만약 CreateUserDTO에 대한 유효성 검사를 하고 싶다고 하자.

@Post()
  async create(@Body(ValidationPipe) dto: CreateUserDTO): Promise<void> {
    const { name, email, password } = dto;
    await this.usersService.createUser(name, email, password);
  }

 위와 같이 요청 객체로 CreateUserDTO를 받으려고 한다.

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

export class CreateUserDTO {
  @IsString()
  readonly name: string;

  @IsEmail()
  readonly email: string;
  
  @IsString()
  readonly password: string;
}

간단한 유효성 검사만 넣었다.

 

요청이 들어올 때 객체가 들어올 것이고,

ValidationPipe는 요청 객체를 위의 CreateUserDTO 클래스로 변환하는 것이다.(plainToClass 메서드를 통해)

순서대로 DTO의 metatype, 요청 객체, plainToClass를 통해 변환된 객체이다.

 

이제 변환된 객체로 유효성 검사를 해야 한다.

 const errors = await validate(object);

validate 메서드를 이용해 변환된 객체가 유효한지 검사한다.

 

    if (errors.length > 0) {
      throw new BadRequestException('Failed');
    }
    return value;

하나라도 유효하지 않다면 예외 처리를 하고,

유효하다면 값을 반환한다.

 

이렇게 class-validator와 class-transformer를 이용해 유효성 검사가 이뤄진다.

유저 서비스에 유효성 검사 적용하기

먼저 필요한 라이브러리를 설치해 준다.

$npm i -D class-validator class-transformer

그리고 Nest에서 제공하는 ValidationPipe를 전역으로 적용한다.

class-transformer가 적용되게 하려면 transform 속성을 true로 줘야 한다.

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

declare const module: any;

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

이제 유저를 생성할 때의 DTO를 수정해 보자.

아래와 같은 유효성 검사를 할 것이다.

  • 사용자 이름은 2자 이상 30자 이하인 문자열
  • 사용자 이메일은 60자 이하의 문자열로서 이메일 주소 형식에 부합
  • 사용자 패스워드는 영문 대소문자와 숫자 또는 특수문자로 이뤄진 8 자 이상 30자 이하 문자열
import {
  IsEmail,
  IsString,
  Matches,
  MaxLength,
  MinLength,
} from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(30)
  readonly name: string;

  @IsString()
  @IsEmail()
  @MaxLength(60)
  readonly email: string;

  @IsString()
  @Matches(/^[A-Za-z/d!@#$%^&*()]{8,30}$/)
  readonly password: string;
}

 

class-transformer에서 가장 많이 사용되는 Transform 데커레이터는 인자를 전달받아 속성을 변형한 후 리턴한다.

Transform 데커레이터의 정의는 아래와 같다.

import { TransformationType } from '../../enums';
import { ClassTransformOptions } from '../class-transformer-options.interface';

export declare function Transform(transformFn: (params: TransformFnParams) => any, options?: TransformOptions): PropertyDecorator;

export interface TransformFnParams {
    value: any;
    key: string;
    obj: any;
    type: TransformationType;
    options: ClassTransformOptions;
}

param의 값을 콘솔에 찍어보자.

위의 TransformParams의 interface대로 찍히는 것을 볼 수 있다.

 

그중 obj 속성에는 현재 속성이 속해 있는 객체를 가리킨다.

즉, name 속성을 가지고 있는 CreateUserDto 객체를 가리킨다.

 

Transform 데커레이터를 이용해 name의 앞 뒤 공백이 있을 때 공백을 지우고 반환해 보자.

  @Transform((param)=>param.value.trim())
  @IsString()
  @MinLength(2)
  @MaxLength(30)
  readonly name: string;

위와 같이 작성하면 name의 앞뒤 공백을 지울 수 있다.


참고

 

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 Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

07장 파이프와 유효성 검사 - 요청이 제대로 전달되었는가

.

wikidocs.net

마치며

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90