-
230105목_공부일지> Backend/NestJS 2023. 1. 5. 17:36
1. 파이프와 유효성 검사: 요청이 제대로 전달되었는가 (p 113~)
=> 정의: 요청이 라우터 핸들러로 전달되기 전에 요청 객체를 변환할 수 있는 기회를 제공
=> 미들웨어와 유사하나, 미들웨어는 app의 모든 컨텍스트에서 사용하도록 할 수 없음.
=> 목적: 변환(입력 데이터를 원하는 형시으로 변환) && 유효성 검사 (입력 데이터가 사용자가 정한 기준에 유효하지 않은 경우 예외)
=> 사용방법: @Param decorator의 두번째 인수로 파이프를 넘겨 현재 실행 콘텍스트 (ExecutionContext에 바인딩)
findOne(@Param('id', new ParseIntPipe({errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE})) id: number) {
=> 숫자일때는 정상 // CMD:: curl -X GET http://localhost:3000/origin/22 // RESULT:: This action returns a #22 user% => 문자일때는 400에러 // CMD:: curl -X GET http://localhost:3000/users/origin/ww // RESULT:: {"statusCode":400,"message":"Validation failed (numeric string is expected)","error":"Bad Request"}%
2. DefaultValuePipe는 인수의 값에 기본값을 설정할 때 사용.
// CMD:: curl -X GET http://localhost:3000/origin // RESULT:: This action returns all users offset:: 0 & limit:: 10% @Get('/origin') findAll( @Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number ) { return this.usersService.findAll(offset, limit); }
3. 파이프의 내부 구현 및 이해
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @Injectable() export class ValidationPipe implements PipeTransform<any> { // 구현되어야 하는 transform은 두개의 parameter 있음. // 1. value: 현재 pipe로 전달되는 인수 // 2. metadata: 현재 파이프에 전달되는 인수의 메타데이터 transform(value: any, metadata: ArgumentMetadata): any { return value; } // ArgumentMetadata 정의 // type: 파이프에 전달된 param의 종류 (Body, Param, Query) // metatype:: 라우트 핸들러에 정의된 인수의 타입/ 핸들러에서 타입을 생략하거나 바닐라 사용시 undefined // data:: decorator에 전달된 문자열 => 매개변수의 이름 }
// CMD:: curl -X GET http://localhost:3000/origin/pipe/23 // RESULT:: This action returns a #22 user% // CONSOLE:: { metatype: [Function: Number], type: 'param', data: 'id' } @Get('/origin/pipe/:id') findOne_validationPipe(@Param('id', ValidationPipe) id: number) { return this.usersService.findOne(+id); }
4. 유효성 검사 파이프 만들기 (using class-validator && class-transformer)
=> npm i --save class-validator class-transformer
=> create-user-dto.ts
// NestJS에서 payload 를 처리하기 위해서는 데이터 전송 객체 (DTO)를 구현하면 된다. import {IsString, MaxLength, MinLength, IsEmail } from "class-validator"; export class CreateUserDto { @IsString() @MinLength(1) @MaxLength(20) readonly name: string; @IsEmail() readonly email: string; readonly password: string; }
=> validation.pipe.ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @Injectable() export class ValidationPipe implements PipeTransform<any> { // 구현되어야 하는 transform은 두개의 parameter 있음. // 1. value: 현재 pipe로 전달되는 인수 // 2. metadata: 현재 파이프에 전달되는 인수의 메타데이터 // before:: // transform(value: any, metadata: ArgumentMetadata): any { // // return value; // } // after:: // CMD :: http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example", "email":"email@example.com"}' // RESULT:: {"name":"name_example","email":"email@example.com"}% async transform(value: any, { metatype }: ArgumentMetadata) { // ArgumentMetadata 정의 // type: 파이프에 전달된 param의 종류 (Body, Param, Query) // metatype:: 라우트 핸들러에 정의된 인수의 타입/ 핸들러에서 타입을 생략하거나 바닐라 사용시 undefined // data:: decorator에 전달된 문자열 => 매개변수의 이름 if (!metatype || !this.toValidate(metatype)) { return value; } //take entity and turn it into instant const object = plainToClass(metatype, value); const errors = await validate(object) if (errors.length > 0) { throw new BadRequestException('Validation failed'); } return value } // 설명:: // 전달된 meta-type 이 파이프가 지원하는 타입인지 검사. (toValidate) // class-transformer plainToClass 함수를 통해 순수 자바스크립트 객체를 클래스의 객체로 변경 => 캡슐화 진행 // (더이상 리터럴 객체를 다룰 필요 없이 값과 행위가 한곳에 모여있는 클래스 인스턴스 단위로 다룰 수 있게 됩니다.) // class-validator 유효성 검사 데커레이터는 타입이 필요. (이를 위해서, plainToClass 사용 ) // 네트워크 요청을 통해 들어온 데이터는 역직렬화 과정에서 본문의 객체가 아무런 타입 정보도 가지고 있지 않기 떄문에, 타입을 지정하는 변환과정을 plainToClass로 수행 // 마지막으로 유효성 검사에 통과했다면, 원래 값 그대로 전달 || 실패시 400 에러 private toValidate(metatype: Function): boolean { const types: Function[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); } }
=> ValidationPipe 적용하기 위해 1) main.ts에 추가 및 2) create핸들러에 적용
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from "./pipe/validation/validation.pipe"; import * as dotenv from 'dotenv'; import * as path from 'path'; // dotenv.config({ // path: path.resolve( // (process.env.NODE_ENV === 'production') ? './env/.production.env' // :(process.env.NODE_ENV === 'stage') ? './env/.stage.env' : './env/.development.env' // ) // }) async function bootstrap() { const app = await NestFactory.create(AppModule); // ValidationPipe를 모든 핸들러에 일일이 지정하지 않고, 전역으로 설정하려면 부트스트랩 과정에서 적용 => nest에 이미 validation-pipe가 있기 때문에 직접 만들필요 X app.useGlobalPipes(new ValidationPipe()) await app.listen(3000); } bootstrap();
@Post('/origin/create') create(@Body(ValidationPipe) createUserDto: CreateUserDto) { return this.usersService.create(createUserDto) }
5. 유저 서비스에 유효성 검사 적용
=> 기존에 생성한 ValidationPipe은 이해를 위해서 만들고 사용했기 때문에, 이제는 @nestjs/common 에 있는 ValidationPipe사용
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; // import { ValidationPipe } from "./pipe/validation/validation.pipe"; import { ValidationPipe } from "@nestjs/common"; import * as dotenv from 'dotenv'; import * as path from 'path'; // dotenv.config({ // path: path.resolve( // (process.env.NODE_ENV === 'production') ? './env/.production.env' // :(process.env.NODE_ENV === 'stage') ? './env/.stage.env' : './env/.development.env' // ) // }) async function bootstrap() { const app = await NestFactory.create(AppModule); // ValidationPipe를 모든 핸들러에 일일이 지정하지 않고, 전역으로 설정하려면 부트스트랩 과정에서 적용 => nest에 이미 validation-pipe가 있기 때문에 직접 만들필요 X // app.useGlobalPipes(new ValidationPipe()) // class-transformer를 사용하기 위해서는 transform: true값 지정 app.useGlobalPipes(new ValidationPipe({ transform: true })) await app.listen(3000); } bootstrap();
6. Class-Transformer 활용.
=> Transform decorator는 transform 함수를 인수로 받고, 여기서 속성의 값과, 그 속성을 가지고 있는 객체를 인수로 받아, 속성 변화하는 함수.
// NestJS에서 payload 를 처리하기 위해서는 데이터 전송 객체 (DTO)를 구현하면 된다. import {IsString, MaxLength, MinLength, IsEmail } from "class-validator"; import {Expose, Transform} from 'class-transformer' export class CreateUserDto { @Transform(params => { // name의 받은 인수가 양옆으로 띄어쓰기 되어있는 경우에 줄여주는 transform return params.value.trim() }) @IsString() @MinLength(1) @MaxLength(20) readonly name: string; @IsEmail() readonly email: string; readonly password: string; }
7. @Transform decorator를 통해 들어오는 param을 변형하여, 조건이 유효하지 않는 경우, throw error를 할 수 있지만, 직접 필요한 검사를 수행하는 decorator를 만들어 사용할 수 있다.
import {registerDecorator, ValidationArguments, ValidationOptions} from "class-validator"; const NotIn = (property: string, validationOptions?: ValidationOptions) => { //데커레이터의 인수는 객체에서 참조하려고하는 다른 속성의 이름과 ValidationOptions 를 받는다 return (object: Object, propertyName: string) => { // registerDecorator를 호출하는 함수 리턴 : 인수로 데커레이터가 선언될 객체와 속성이름 받음 registerDecorator({ // registerDecorator 함수는 ValidationDecoratorOptions 객체를 인수로 받음 name: 'NotIn', // Decorator 이름 target: object.constructor, // Decorator는 객체가 생성될때 적용 propertyName, options: validationOptions, // 유효성 옵션은 Decorator의 인수로 전달받은 것을 사용 constraints: [property], validator: { // 유효성 검사 규칙 (id와 pw가 비슷한 경우, ERROR) validate(value: any, args: ValidationArguments){ const [relatedPropertyName] = args.constraints; const relatedValue = (args.object)[relatedPropertyName] // console.log(args.object) => createUserDTO 값을 음 return typeof value === 'string' && typeof relatedValue === 'string' && !relatedValue.includes(value) } } }) } } export default NotIn;
=> create-user-dto.ts 수정
@Transform(params => { return params.value.trim() }) @NotIn('password', {message: 'password 체크'}) @IsString() @MinLength(1) @MaxLength(20) readonly name: string;
1. cmd:: curl -X POST http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example ", "email":"email@example.com","password":"1234"}' res:: {"name":"name_example","email":"email@example.com","password":"1234"}% 2. cmd:: curl -X POST http://localhost:3000/origin/create -H "Content-Type: application/json" -d '{"name": "name_example ", "email":"email@example.com","password":"name_example_pw"}' res:: {"statusCode":400,"message":["password 체크"],"error":"Bad Request"}%
'> Backend > NestJS' 카테고리의 다른 글
230102월_공부일지 (0) 2023.01.02 20221229목_공부일지 (0) 2022.12.30 221228수_공부일지 (0) 2022.12.29 20221227화_공부 일지 (0) 2022.12.28 20221222Thu_공부 일지 (0) 2022.12.23