현재 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이 됩니다. 따라서 변형을 해줘야합니다.
@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); // 변경
}
이미 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";
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를 붙입니다.
@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
}
따라서 큰 자유도를 가지고 있습니다.
@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() 부분은 지워주세요!
현재 /auth/pipe/password.pipe.ts에는 PasswordPipe가 1개 존재합니다. 근데 만약 길이에 대한 제한을 반복적으로 사용할 것 같은 경우, 다른 방법을 만들 수 있습니다.
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를 해서 생성을 하게 됩니다.
@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
}