NestJS-Pipe

jaegeunsong97·2023년 11월 20일
0

NestJS

목록 보기
13/37
post-custom-banner

🖊️Pipe, ParseIntPipe

현재 controller, service, repository 까지 기본적인 것들은 배웠습니다. 이제는 Pipe에 대해서 배워봅시다. 공식문서 사이트를 가봅시다.

@Injectable로 데코레이션이 되어있습니다. 즉 Provider라는 의미입니다. 그리고 PipeTransform을 interface한다라고 되어있습니다. OOP에서 implement한다는 것입니다.

그리고 Pipe의 역할은 변형, 검증입니다. 이 Pipe들은 @nestjs/common 페키지에서 전부 불러올 수 있습니다.

ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe

일단 현재 우리의 프로젝트 api에는 큰 문제가 있습니다. url에서 받아오기 때문에 반드시 string이 됩니다. 따라서 변형을 해줘야합니다.

  • posts.controller.ts
@Get(':id')
getPost(@Param('id') id: string) {
  	return this.postsService.getPostById(+id);
}
.
.
변경
.
.
@Get(':id')
getPost(@Param('id', ParseIntPipe) id: number) {  // 변경
  return this.postsService.getPostById(id);  // 변경
}

포스트맨으로 테스트를 합시다.

{
    "id": 1,
    "title": "Flutter",
    "content": "Flutter 내용",
    "likeCount": 0,
    "commentCount": 0,
    "author": {
        "id": 2,
        "nickname": "codefactory10",
        "email": "codefactory10@codefactory.ai",
        "password": "$2b$10$Z3g0/XLI1BFW1ZvJuxPtWexLaFgnTJSBLX3PIYOes8Fq0L/bRk7yq",
        "role": "USER"
    }
}

만약 숫자가 아닌 string형태로 보내면 에러를 던져줍니다.

{
    "message": "Validation failed (numeric string is expected)",
    "error": "Bad Request",
    "statusCode": 400
}

만약 코드에서 ParseIntPipe만 제거를 하면 전혀 다른 에러가 나옵니다.

@Get(':id')
getPost(@Param('id') id: number) {
 	return this.postsService.getPostById(id);
}
{
    "statusCode": 500,
    "message": "Internal server error"
}

즉, 서버 어디선가 내부에서 터진 것입니다. 따라서 ParseIntPipe를 붙여서 변형을 해줍시다. 그리고 posts.controller.ts에서 전부 똑같이 바꿔줍니다.

@Put(':id')
putPost(
    @Param('id', ParseIntPipe) id: number, // 변경
    @Body('title') title?: string,
    @Body('content') content?: string,
) {
	return this.postsService.updatePost(
   		id, title, content,  // 변경
    ;
}

@Delete(':id')
deletePost(@Param('id', ParseIntPipe) id: number) {  // 변경
  	return this.postsService.deletePost(id);  // 변경
}

🖊️Custom Pipe

이미 nest.js에서 제공해주는 Pipe가 없는 경우, 직접 만들어야 합니다. 예를 들면 아래의 코드에서는 비밀번호가 제약이 없는 상태입니다. 즉 string의 길이를 체크하는 Pipe를 만들어야 하는 것입니다.

@Post('register/email')
postRegisterEmail(
    @Body('nickname') nickname: string,
    @Body('email') email: string,
    @Body('password') password: string, // 비밀번호 작성시 제약이 필요!!
) {
    return this.authService.registerWithEmail({
        email,
        nickname,
        password,
    })
}

그리고 custom pipe를 만들 때, 반드시 넣어줘야하는 import가 있습니다.

import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException } from "@nestjs/common";
  • auth/pipe/password.pipe.ts

custome pipe를 특정 폴더에 만듭니다.

import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException } from "@nestjs/common"; // 반드시 필요한 import

// https://docs.nestjs.com/pipes#custom-pipes
@Injectable() // 공식문서에 PipeTransform을 implements 해야한다.
export class PasswordPipe implements PipeTransform { 
   /**
    * value : 받는 값
    * metadata : 
    *   Paramtype(body, query, param), 
    *   metatype(id: string할때 string 의미), 
    *   data(@Body('userId')라면 userId)
    */
  	if (value.toString().length > 8) throw new BadRequestException('비밀번호는 8자 이하로 입력해주세요.!');
	return value.toString(); 
}

그리고 auth/auth.controller.ts에 custom pipe를 붙입니다.

  • auth/auth.controller.ts
@Post('register/email')
postRegisterEmail(
    @Body('nickname') nickname: string,
    @Body('email') email: string,
    @Body('password', PasswordPipe) password: string, // PasswordPipe에서 걸리면 아래의 로직은 실행되지 않는다.
) {
    return this.authService.registerWithEmail({
        email,
        nickname,
        password,
    })
}

포스트맨으로 테스트를 해봅시다. 올바른 길이를 입력하면 회원가입이 가능합니다.

하지만 8자 이상을 넘으면 직접 만든 custom pipe가 발동해서 에러를 보냅니다. 즉, PasswordPipe에서 잡혔기 때문에 그 밑에 존재하는 코드는 절대로 실행되지 않습니다.

{
    "message": "비밀번호는 8자 이하로 입력해주세요.!",
    "error": "Bad Request",
    "statusCode": 400
}

따라서 큰 자유도를 가지고 있습니다.


🖊️DefaultValuePipe

@Post()
postPosts(
    @Body('authorId') authorId: number, 
    @Body('title') title: string, 
    @Body('content') content: string,
    @Body('isPublic') isPublic: boolean, // 임시 추가
) {
    return this.postsService.createPost(authorId, title, content);
}

포스트맨으로 실행 후 디버깅을 하면 다음과 같이 isPublic은 undefined가 됩니다.

따라서 디폴트로 true가 되도록 만들어줍니다.

@Post()
postPosts(
    @Body('authorId') authorId: number, 
    @Body('title') title: string, 
    @Body('content') content: string,
    @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean, // 변경
) {
    return this.postsService.createPost(authorId, title, content);
}

여기서 질문이 나올 수 있습니다. 왜 new를 할까? 이유는 DefaultValuePipe는 매번 새롭게 생성을 하는 것입니다.

반면 ParseIntPipe과 같은 이미 @nestjs/common 패키지에 존재하거나 custom Pipe는 Nest.js에서 IoC에 등록 후, DI를 해주는 것이기 때문에 매번 새롭게 생성되지 않습니다.

isPublic() 부분은 지워주세요!

🖊️여러 Pipe 적용

현재 /auth/pipe/password.pipe.ts에는 PasswordPipe가 1개 존재합니다. 근데 만약 길이에 대한 제한을 반복적으로 사용할 것 같은 경우, 다른 방법을 만들 수 있습니다.

  • auth/pipe/password.pipe.ts
import { Injectable, PipeTransform, ArgumentMetadata, BadRequestException } from "@nestjs/common";

@Injectable() // https://docs.nestjs.com/pipes#custom-pipes
export class PasswordPipe implements PipeTransform {  // 공식문서에 PipeTransform을 implements 해야한다.
     transform(value: any, metadata: ArgumentMetadata) {
          /**
           * value : 받는 값
           * metadata : 
           *   Paramtype(body, query, param), 
           *   metatype(id: string할때 string 의미), 
           *   data(@Body('userId')라면 userId)
           */
          if (value.toString().length > 8) throw new BadRequestException('비밀번호는 8자 이하로 입력해주세요.!');
          return value.toString(); 
     }
}

@Injectable()
export class MaxLengthPipe implements PipeTransform {
     constructor(private readonly length: number) {} 
     transform(value: any, metadata: ArgumentMetadata) {
         if (value.toString().length > this.length) throw new BadRequestException(`최대 길이는 ${this.length}입니다.`);
         return value.toString();
     }
}

@Injectable()
export class MinLengthPipe implements PipeTransform {
     constructor(private readonly length: number) {}
     transform(value: any, metadata: ArgumentMetadata) {
         if (value.toString().length < this.length) throw new BadRequestException(`최소 길이는 ${this.length}입니다.`);
         return value.toString();
     }
}

이제 생성한 MaxLengthPipe와 MinLengthPipe를 가지고 동시에 적용을 할 수 있습니다. 또한 이 2개의 pipe는 constructor를 가지고 있기 때문에 new를 해서 생성을 하게 됩니다.

  • auth.controller.ts
@Post('register/email')
postRegisterEmail(
    @Body('nickname') nickname: string,
    @Body('email') email: string,
    @Body('password', PasswordPipe) password: string,
) {
    return this.authService.registerWithEmail({
        email,
        nickname,
        password,
    })
}
.
.
변경
.
.
@Post('register/email')
postRegisterEmail(
    @Body('nickname') nickname: string,
    @Body('email') email: string,
    @Body('password', new MaxLengthPipe(8, '비밀번호'), new MinLengthPipe(3, '비밀번호')) password: string, // new
) {
    return this.authService.registerWithEmail({
        email,
        nickname,
        password,
    })
}

이제 포스트맨으로 테스트를 해봅시다.

{
    "message": "최대 길이는 8입니다.",
    "error": "Bad Request",
    "statusCode": 400
}

{
    "message": "최소 길이는 3입니다.",
    "error": "Bad Request",
    "statusCode": 400
}

여기서 알아야 할 것은 Pipe를 세부화 해서 사용이 가능하며, PasswordPipe보다 일반화되었기 때문에 어디서든 사용이 가능한 편리성이 있습니다. 즉, 조합을 해서 validation 체크를 하는 것입니다.

📍부연설명

추가적인 설명입니다. Pipe constructor에 파라미터값을 추가를 하게되면 여러 경우의 에러처리가 가능합니다.

constructor(
    private readonly length: number,
    private readonly subject: string,
) {}

transform(value: any, metadata: ArgumentMetadata) {
    if (value.toString().length < this.length) throw new BadRequestException(`${this.subject} 길이는 ${this.length}입니다.`);
    return value.toString();
}
.
.
컨트롤러
.
.
@Post('register/email')
postRegisterEmail(
      @Body('nickname') nickname: string,
      @Body('email') email: string,
      @Body('password', new MaxLengthPipe(8), new MinLengthPipe(3, '무조건 최소 길이')) password: string, // 임시 변경
  ) {
    return this.authService.registerWithEmail({
        nickname, 
        email,
        password,
    });
}
{
    "message": "무조건 최소 길이 길이는 3입니다.",
    "error": "Bad Request",
    "statusCode": 400
}
profile
블로그 이전 : https://medium.com/@jaegeunsong97
post-custom-banner

0개의 댓글