NestJS Request Lifecycle (4) - Pipe

2022. 12. 29. 13:52ㆍBackend/NestJS

이전 κΈ€


νŒŒμ΄ν”„

NestJSμ—μ„œ νŒŒμ΄ν”„λž€ 컨트둀러의 μ•žλ‹¨μ—μ„œ μ‚¬μš©μžμ˜ μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 역할을 μˆ˜ν–‰ν•˜λŠ” 생λͺ…μ£ΌκΈ°λ₯Ό μ˜λ―Έν•œλ‹€.

  • λ³€ν™˜: μ‚¬μš©μžμ˜ μš”μ²­ 속 데이터λ₯Ό μ›ν•˜λŠ” ν˜•νƒœλ‘œ λ³€κ²½ ν›„ μ»¨νŠΈλ‘€λŸ¬μ— λ„˜κ²¨μ€€λ‹€.
  • 검증: μ‚¬μš©μžμ˜ μš”μ²­ 속 데이터에 λŒ€ν•΄, 컨트둀러 단에 λ„λ‹¬ν•˜κΈ° μ „ 검증을 μˆ˜ν–‰ν•˜κ³  검증이 μ‹€νŒ¨ν•  μ‹œ 이λ₯Ό 컨트둀러 단에 λ„˜κΈ°μ§€ μ•Šκ³  λ°”λ‘œ μ‚¬μš©μžμ—κ²Œ μ—λŸ¬λ₯Ό λ°˜ν™˜ν•œλ‹€.

νŒŒμ΄ν”„μ˜ 쑴재 이유?

NestJS의 μ•„ν‚€ν…μ³μ—μ„œ νŒŒμ΄ν”„κ°€ λ”°λ‘œ μ‘΄μž¬ν•˜λŠ” μ΄μœ λŠ” λ°”λ‘œ 관심사 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•¨μ΄λ‹€. νŒŒμ΄ν”„μ—μ„œ μˆ˜ν–‰ν•˜λŠ” λ‘œμ§μ€ 컨트둀러 내에 μœ„μΉ˜ν•΄λ„ λ˜μ§€λ§Œ, 그럴 경우 컨트둀러의 관심사가 λͺ¨ν˜Έν•΄μ§ˆ 수 있으며 λΉ„μŠ·ν•œ λ°μ΄ν„°μ˜ 검증에 λŒ€ν•΄ μž¬μ‚¬μš©μ„±λ„ λ‚˜λΉ μ§„λ‹€. λ”°λΌμ„œ NestJSμ—μ„œλŠ” 메인 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 μ˜€μ—Όμ„ λ°©μ§€ν•˜κ³ μž 데이터 검증 κ΄€λ ¨ λ‘œμ§μ„ νŒŒμ΄ν”„μ— λΆ„λ¦¬ν•˜μ—¬ μž‘μ„±ν•œλ‹€.

 

νŒŒμ΄ν”„ μž‘μ„±

NestJSμ—μ„œλŠ” 기본적으둜 μ•„λž˜ 9μ’…μ˜ 기본적으둜 μ œκ³΅λ˜λŠ” νŒŒμ΄ν”„λΌμΈμ΄ μ‘΄μž¬ν•˜κ³ , 이λ₯Ό ν™œμš©ν•˜λ©΄ λŒ€λΆ€λΆ„μ˜ λ³€ν™˜, 검증 λ‘œμ§μ„ κ΅¬ν˜„ν•  수 μžˆλ‹€. (μ•„λž˜ νŒŒμ΄ν”„λ“€μ˜ 세뢀적인 μ •λ³΄λŠ” κ³΅μ‹λ¬Έμ„œ μ°Έμ‘°)

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

 λ‹€λ§Œ, 기본적으둜 μ œκ³΅λ˜λŠ” νŒŒμ΄ν”„λ‘œλŠ” μ„œλΉ„μŠ€ λ‘œμ§μ„ κ΅¬ν˜„ν•˜κΈ° λΆ€μ‘±ν•˜λ‹€λ©΄ 직접 μ»€μŠ€ν…€ νŒŒμ΄ν”„λ₯Ό μƒμ„±ν•΄μ„œ ν™œμš©ν•  μˆ˜λ„ μžˆλ‹€.

μ»€μŠ€ν…€ νŒŒμ΄ν”„ μž‘μ„±

μ»€μŠ€ν…€ νŒŒμ΄ν”„λ₯Ό μž‘μ„±ν•˜κΈ° μœ„ν•΄μ„œλŠ” NestJS의 PipeTransform을 μƒμ†ν•œ ν΄λž˜μŠ€μ™€ 클래슀 λ‚΄ transform λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•΄μ•Ό ν•œλ‹€.

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

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value; // λͺ¨λ“  valueλ₯Ό ν†΅κ³Όμ‹œν‚€λŠ” νŒŒμ΄ν”„
  }
}

μœ„ μ˜ˆμ‹œλŠ” κ³΅μ‹λ¬Έμ„œμ˜ λͺ¨λ“  값을 ν†΅κ³Όμ‹œν‚€λŠ” νŒŒμ΄ν”„ μ˜ˆμ‹œμ΄λ‹€. transform λ©”μ„œλ“œλŠ” 2개의 인자λ₯Ό κ°–λŠ”λ‹€. 첫번째 인자인 valueλŠ” μ»¨νŠΈλ‘€λŸ¬μ—μ„œ μ²˜λ¦¬λ˜λŠ” 인자이고(λ‹€λ§Œ μ»¨νŠΈλ‘€λŸ¬μ— λ„λ‹¬ν•˜μ§€λŠ” λͺ»ν•œ μƒνƒœ), λ‘λ²ˆμ§Έ 인자인 metadataλŠ” μ•žμ„  value의 메타데이터이닀. 

μ»€μŠ€ν…€ νŒŒμ΄ν”„μ—μ„œμ˜ μ˜ˆμ™Έ 처리

νŒŒμ΄ν”„ λ‚΄μ—μ„œ λ°œμƒν•˜λŠ” 각쒅 μ—λŸ¬λ“€μ€ μ „λΆ€ NestJS의 ν•„ν„° λ ˆμ΄μ–΄ λ‚΄μ—μ„œ λ°œμƒν•œλ‹€. λ”°λΌμ„œ νŒŒμ΄ν”„ λ‚΄μ—μ„œ 검증 둜직이 μ‹€νŒ¨ν•˜μ—¬ 그에 따라 μ—λŸ¬λ₯Ό λ°˜ν™˜ν•΄λ„ NestJS의 ν•„ν„°κ°€ 잘 μž‘λ™ν•œλ‹€. 결과적으둜 검증이 μ‹€νŒ¨ν–ˆμ„ λ•Œμ˜ μ˜ˆμ™Έ μ²˜λ¦¬λŠ” ν•„ν„° λ‘œμ§μ— μœ„μž„ν•˜κ³ , νŒŒμ΄ν”„ λ‹¨μ—μ„œλŠ” μ—λŸ¬ 상황에 λ§žλŠ” μ»€μŠ€ν…€ μ—λŸ¬λ₯Ό λ˜μ§€κΈ°λ§Œ ν•˜λ©΄ λœλ‹€.

NestJSμ—μ„œ 기본적으둜 μ œκ³΅λ˜λŠ” νŒŒμ΄ν”„κ°€ κ°•λ ₯ν•˜κ³ , μ»€μŠ€ν…€ νŒŒμ΄ν”„λ₯Ό μž‘μ„±ν•˜λŠ” 방법은 κ³΅μ‹λ¬Έμ„œμ— 잘 λ‚˜μ™€μžˆκΈ° λ•Œλ¬Έμ— 이에 λŒ€ν•΄μ„œλŠ” λ‹€μŒμ˜ 링크λ₯Ό μ°Έκ³ λ°”λž€λ‹€.

https://docs.nestjs.com/pipes#built-in-pipes

 

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

 

νŒŒμ΄ν”„ 적용

NestJSμ—μ„œ νŒŒμ΄ν”„λ₯Ό μ μš©ν•˜λŠ” 방식은 크게 4가지가 μ‘΄μž¬ν•œλ‹€.

  1. λͺ¨λ“ˆ 밖에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ νŒŒμ΄ν”„
  2. λͺ¨λ“ˆ 내에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ νŒŒμ΄ν”„
  3. 컨트둀러 λ©”μ„œλ“œ(ν•Έλ“€λŸ¬) λ ˆλ²¨μ— μž‘μš©ν•˜λŠ” νŒŒμ΄ν”„
  4. λ©”μ„œλ“œ νŒŒλΌλ―Έν„°(인자) λ ˆλ²¨μ— μž‘μš©ν•˜λŠ” νŒŒμ΄ν”„

λͺ¨λ“ˆ 밖에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ νŒŒμ΄ν”„

ν•΄λ‹Ή 방식을 μœ„ν•΄μ„œ NestJS μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ useGlobalPipes() λ©”μ„œλ“œλ₯Ό μ΄μš©ν•  수 μžˆλ‹€. 이 방식은 λͺ¨λ“ˆ 밖에 νŒŒμ΄ν”„λ₯Ό μœ„μΉ˜μ‹œν‚€κΈ° λ•Œλ¬Έμ— λ‹€λ₯Έ λͺ¨λ“ˆμ„ 가져와 μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•˜μ—¬ μ‚¬μš©ν•˜κ±°λ‚˜, ν˜Ήμ€ λ°˜λŒ€μ˜ κ²½μš°κ°€ λΆˆκ°€λŠ₯ν•˜λ‹€. μ½”λ“œλŠ” λ‹€μŒκ³Ό 같이 μž‘μ„±ν•œλ‹€.

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // μ „μ—­ νŒŒμ΄ν”„ μ„€μ •, κΈ°λ³Έ 제곡 νŒŒμ΄ν”„λ₯Ό ν™œμš©
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

ν•΄λ‹Ή 방식을 μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” νŒŒμ΄ν”„ 클래슀의 μΈμŠ€ν„΄μŠ€λ₯Ό 직접 인자둜 μΆ”κ°€ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.

λͺ¨λ“ˆ 내에 μœ„μΉ˜ν•˜λŠ” μ „μ—­ νŒŒμ΄ν”„

ν•΄λ‹Ή 방식을 ν†΅ν•˜λ©΄ νŒŒμ΄ν”„ λ‚΄μ—μ„œ λ‹€λ₯Έ λͺ¨λ“ˆμ˜ μ˜μ‘΄μ„±μ„ μ£Όμž…ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆλ‹€. ν•΄λ‹Ή 방식은 메인 λͺ¨λ“ˆκ³Ό μ—°κ²°λœ μ–΄λ–€ λͺ¨λ“ˆμ—μ„œ μ£Όμž…μ΄ λ˜λ”λΌλ„ μ „μ—­μœΌλ‘œ λ™μΌν•˜κ²Œ μ‚¬μš©ν•  수 있기 λ•Œλ¬Έμ—, κ³΅μ‹λ¬Έμ„œμ—μ„œλŠ” νŒŒμ΄ν”„κ°€ μƒμ„±λœ λͺ¨λ“ˆμ—μ„œ λ°”λ‘œ μ£Όμž…ν•  것을 μΆ”μ²œν•˜κ³  μžˆλ‹€.

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

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

컨트둀러 λ©”μ„œλ“œ(ν•Έλ“€λŸ¬) λ ˆλ²¨μ—μ„œ μž‘μš©ν•˜λŠ” νŒŒμ΄ν”„

ν•΄λ‹Ή 방식은 NestJS의 @UsePipes() λ°μ½”λ ˆμ΄ν„°λ₯Ό ν™œμš©ν•˜λŠ” 방식이닀. 이λ₯Ό ν†΅ν•˜λ©΄ νŠΉμ • ν•Έλ“€λŸ¬μ— λ“€μ–΄μ˜€λŠ” 데이터에 νŒŒμ΄ν”„λ₯Ό μ μš©ν•  수 μžˆλ‹€. ν•΄λ‹Ή 방식은 λ°μ½”λ ˆμ΄ν„°μ— μ μš©ν•  νŒŒμ΄ν”„ 클래슀λ₯Ό λ„£κ±°λ‚˜ ν˜Ήμ€ μΈμŠ€ν„΄μŠ€λ₯Ό λ„£λŠ” λ°©μ‹μœΌλ‘œ 적용이 κ°€λŠ₯ν•˜λ‹€. λ§Œμ•½ 클래슀의 μƒμ„±μžμ˜ 인자λ₯Ό 톡해 νŒŒμ΄ν”„μ˜ 역할을 μ •μ˜ν•΄μ•Ό ν•œλ‹€λ©΄, μΈμŠ€ν„΄μŠ€ 방식을 ν™œμš©ν•  수 μžˆλ‹€.

import {
  Body,
  Controller,
  Post,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { SignupDto } from '@auth/dto/signup.dto';
import { AuthService } from '@auth/auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('signup')
  @UsePipes(ValidationPipe) // λΌμš°ν„° ν•Έλ“€λŸ¬μ— νŒŒμ΄ν”„ 적용
  async signup(@Body() signupDto: SignupDto) {
    const signupResult = await this.authService.signup(signupDto);
    return signupResult;
  }
}

ν•Έλ“€λŸ¬ νŒŒλΌλ―Έν„°(인자) λ ˆλ²¨μ—μ„œ μž‘μš©ν•˜λŠ” νŒŒμ΄ν”„

ν•΄λ‹Ή 방식은 ν•Έλ“€λŸ¬μ˜ νŠΉμ • νŒŒλΌλ―Έν„°μ— λŒ€ν•΄μ„œλ§Œ νŒŒμ΄ν”„λ₯Ό μ μš©ν•˜λŠ” 방식이닀. NestJSμ—μ„œ μ œκ³΅ν•˜λŠ” 각쒅 νŒŒλΌλ―Έν„° λ°μ½”λ ˆμ΄ν„° (ex. @Body(), @Param(), @Query())λ‚˜ ν˜Ήμ€ NestJS의 customParamDecorator() ν•¨μˆ˜λ₯Ό 톡해 λ§Œλ“€μ–΄μ§„ μ»€μŠ€ν…€ λ°μ½”λ ˆμ΄ν„°μ— λŒ€ν•΄ μ μš©λœλ‹€. (이 λ•Œ, customParamDecorator에 NestJS의 ValidationPipeλ₯Ό μ μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ„€μ •μœΌλ‘œ λ‹€μŒκ³Ό 같이 μž‘μ„±ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.)

@CustomParam(new ValidationPipe({ validateCustomDecorators: true }))

기쑴에 μ‘΄μž¬ν•˜λŠ” νŒŒλΌλ―Έν„° λ°μ½”λ ˆμ΄ν„°λ₯Ό μ‚¬μš©ν•˜λŠ” κ²½μš°μ—λŠ” λ‹€μŒκ³Ό 같이 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ£Όλ©΄ λœλ‹€.

import {
  Body,
  Controller,
  Post,
  ValidationPipe,
} from '@nestjs/common';
import { SignupDto } from '@auth/dto/signup.dto';
import { AuthService } from '@auth/auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('signup')
  async signup(@Body(ValidationPipe) signupDto: SignupDto) {
    const signupResult = await this.authService.signup(signupDto);
    return signupResult;
  }
}

 

DTO λ„μž…(Data Transfer Object)

NestJS κ³΅μ‹λ¬Έμ„œμ—μ„œλŠ” νŒŒμ΄ν”„μ˜ ν™œμš©μ— DTOλ₯Ό μ μš©ν•˜λŠ” 방식을 μ„€λͺ…ν•˜κ³  μžˆλ‹€. 

μ—¬κΈ°μ—μ„œ DTOλž€ Data Transfer Object의 μ•½μžλ‘œ ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλ²„ κ°„, ν˜Ήμ€ μ„œλ²„ λ‚΄ 각 λ ˆμ΄μ–΄ κ°„ 데이터 톡신 μ‹œ λ°μ΄ν„°μ˜ ꡬ쑰λ₯Ό κ°•μ œν•˜λŠ” 역할을 ν•œλ‹€. DTOλŠ” νƒ€μž…μŠ€ν¬λ¦½νŠΈ μΈν„°νŽ˜μ΄μŠ€κ°€ μ•„λ‹Œ 클래슀둜 μ„ μ–Έλ˜μ–΄μ•Ό ν•˜λŠ”λ°, κ·Έ μ΄μœ λŠ” λŸ°νƒ€μž„μ—μ„œλ„ 계속 μž‘λ™μ„ ν•΄μ•Όν•˜κΈ° λ•Œλ¬Έμ΄λ‹€. (μΈν„°νŽ˜μ΄μŠ€λ‘œ μ„ μ–Έ μ‹œ μ‹€ν–‰ μ „ νƒ€μž… μ²΄ν‚Ήλ§Œμ„ 도와쀄 뿐 λŸ°νƒ€μž„μ—λŠ” 영ν–₯을 λ―ΈμΉ˜μ§€ λͺ»ν•œλ‹€.)

이λ₯Ό ν†΅ν•˜λ©΄ νŒŒμ΄ν”„λ₯Ό μ†μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆλ‹€.

λ‹€μŒκ³Ό 같이 DTOλ₯Ό μ„ μ–Έν•  수 μžˆλ‹€.

// signup.dto.ts
import { IsEmail, IsString } from 'class-validator';

export class SignupDto {
  @IsEmail() // email 검증을 μœ„ν•œ λ°μ½”λ ˆμ΄ν„°
  readonly email: string;

  @IsString() // λ¬Έμžμ—΄ 검증을 μœ„ν•œ λ°μ½”λ ˆμ΄ν„°
  readonly password: string;
}

이λ₯Ό νŠΉμ • 컨트둀러의 λ©”μ„œλ“œμ— μ μš©ν•˜μ—¬ νŒŒμ΄ν”„λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ‹€μŒκ³Ό 같이 μž‘μ„±ν•  수 μžˆλ‹€.

import {
  Body,
  Controller,
  Post,
  ValidationPipe
} from '@nestjs/common';
import { SignupDto } from '@auth/dto/signup.dto';
import { AuthService } from '@auth/auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('signup')
  @UsePipes(ValidationPipe)
  async signup(@Body() signupDto: SignupDto) {
    const signupResult = await this.authService.signup(signupDto);
    return signupResult;
  }
}

μœ„μ™€ 같이 μž‘μ„±ν•˜λ©΄ SignupDtoμ—μ„œ μž‘μ„±ν•œ κ΅¬μ‘°λŒ€λ‘œ νŒŒμ΄ν”„ λ‹¨μ—μ„œ 검증이 이루어진닀.

λ§Œμ•½ DTOλ₯Ό μ΄μš©ν•˜μ—¬ λ°μ΄ν„°μ˜ λ³€ν™˜λ„ μˆ˜ν–‰ν•˜κ³  μ‹Άλ‹€λ©΄ μ•„λž˜μ™€ 같이 μž‘μ„±ν•  수 μžˆλ‹€. μ•„λž˜μ˜ μ˜ˆμ‹œλŠ” λΉ„λ°€λ²ˆν˜Έλ₯Ό μˆ«μžν˜•μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ μž…λ ₯ 받을 λ•Œ μ΄μš©ν•  수 μžˆλ‹€.

import { Transform } from 'class-transformer';
import { IsEmail, IsNumber } from 'class-validator';

export class SignupDto {
  @IsEmail()
  readonly email: string;

  @Transform(({ value }) => Number(value)) // numberν˜•μœΌλ‘œ λ³€ν™˜ μˆ˜ν–‰
  @IsNumber()
  readonly password: number;
}

이 λ•Œ 이λ₯Ό NestJS의 ValidationPipeλ₯Ό 톡해 μ μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ‹€μŒμ˜ 섀정을 μΆ”κ°€ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.

// ν΄λž˜μŠ€κ°€ μ•„λ‹Œ μΈμŠ€ν„΄μŠ€λ‘œ 섀정을 μΆ”κ°€ν•˜μ—¬ νŒŒμ΄ν”„ 섀정을 ν•΄μ£Όμ–΄μ•Ό 함
// transform : true
// ValidationPipeμ—μ„œ λ³€ν™˜ μž‘μ—…κΉŒμ§€ μˆ˜ν–‰ν•΄μ€Œ(ν˜•λ³€ν™˜, κΈ°λ³Έκ°’ μ„€μ •)
new ValidationPipe({transform: true})

'Backend > NestJS' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

NestJS Request Lifecycle (5) - Exception Filter  (0) 2023.01.13
NestJS Request Lifecycle (3) - Interceptor  (0) 2022.12.26
NestJS Request Lifecycle (2) - Guard  (0) 2022.12.23
NestJS Request Lifecycle (1) - Middleware  (0) 2022.12.22
NestJS Request Lifecycle (0) - κ°œμš”  (0) 2022.12.22