1. Pipe ์๊ฐ & ParseIntPipe

RequestMiddleware- Global Middleware
- Module Middleware
Guard- Global Guards
- Controller Guards
- Route Guards
Interceptors- Global Interceptors
- Controller Interceptors
- Route Interceptors
Pipes- Global Pipes
- Controller Pipes
- Route Pipes
- Route Parameter Pipes
ControllerServiceInterceptor- Router Interceptor
- Controller Interceptor
- Global Interceptor
Exception Filters- Route
- Controller
- Global
Response
1.1 Pipe๋?
- Pipe๋?
@injectable๋ฐ์ฝ๋ ์ดํฐ๋ ํด๋์ค๋ค.PipeTransform์ธํฐํ์ด์ค๋ฅผ ์์(implements)๋ฐ๋๋ค.- cf. https://docs.nestjs.com/pipes
- ํ์ดํ๋ Controller์ ์ ๊ณต๋๋ argument๋ค์ ์ ์ฉ๋๋ค.
- ์ด argument์๋
@Body, @Param๋ฑ ์ด๋ฏธ ์ฌ์ฉํ๊ณ ์๋ ์ ๋ ฅ๋ฐ๋ Annotation๋ค์ด ๋ชจ๋ ํฌํจ๋๋ค. - Pipe๋ argument ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณตํ ํ Controller ๋ฉ์๋๋ก ๊ฐ๋ค์ ๋๊ฒจ์ค๋ค
- ์ด argument์๋
ํ์ดํ๋ 2๊ฐ์ง ์ฌ์ฉ ์ฌ๋ก๊ฐ ์๋ค.
transformation(๋ณํ): ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ํ์์ผ๋ก ๋ณํ(e.g. ๋ฌธ์์ด์์ ์ ์๋ก)validation(๊ฒ์ฆ): ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ํ๊ฐํ๊ณ ์ ํจํ ๊ฒฝ์ฐ, ๋ณ๊ฒฝํ์ง ์๊ณ ๊ทธ๋๋ก ์ ๋ฌํ๋ค.- ๊ทธ๋ ์ง ์์ผ๋ฉด ์์ธ๋ฅผ throwํ๋ค.
1.2 Global Pipe
1import { ValidationPipe } from '@nestjs/common'2import { NestFactory } from '@nestjs/core'3import { AppModule } from './app.module'45async function bootstrap() {6const app = await NestFactory.create(AppModule)7app.useGlobalPipes(new ValidationPipe())8await app.listen(3000)9}10bootstrap()
1.3 Controller Pipe
1@Controller('movie')2@UsePipes(new ValidationPipe())3export class MovieController {4constructor(private readonly movieService: MovieService) {}56@Get()7getMovies(@Query('title') title?: string) {8return this.movieService.findAll(title)9}10}
1.4 Route Pipe
1@Controller('movie')2export class MovieController {3constructor(private readonly movieService: MovieService) {}45@Patch(':id')6@UsePipes(new ValidationPipe())7patchMovie(@Param('id') id: string, @Body() body: UpdateMovieDto) {8return this.movieService.update(+id, body)9}10}
1.5 Route Parameter Pipe
1@Controller('movie')2export class MovieController {3constructor(private readonly movieService: MovieService) {}45@Patch(':id')6patchMovie(@Param('id', ParseIntPipe) id: number, @Body() body: UpdateMovieDto) {7return this.movieService.update(id, body)8}9}
1.6 ๊ธฐ๋ณธ Pipe ํ์
ValidationPipe: ๋ชจ๋ Validation Annotation์ด ์ ์ฉ๋๋๋ก ํด์ค๋คParseIntPipe: Int ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋คParseFloatPipe: Float ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋คParseBoolPipe: Bool ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋ค.ParseArrayPipe: Array ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋คParseUUIDPipe: UUID ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋คParseEnumPipe: Enum ๊ฐ์ผ๋ก ๋ณํ, ๊ฒ์ฆํ๋ค.DefaultValuePipe: ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ๋คParseFilePipe: ํ์ผ์ ๊ฒ์ฆํ๋ค
์ ํ์ดํ๋ค์ @nestjs/common ํจํค์ง์์ ํธ์ถ ๊ฐ๋ฅํ๋ค.
์ด๋ค ๋ฐ์ฝ๋ ์ดํฐ์ด๋ , 2๋ฒ์จฐ ํ๋ผ๋ฏธํฐ์๋ค๊ฐ ๊ฒ์ฆํ๊ณ ์ถ์ ํํ๋ฅผ ๋ฃ์ผ๋ฉด ๋๋ค.
1// posts.controller.ts ์๋ต2/*** 2) GET /posts/:id3* id์ ํด๋นํ๋ post๋ฅผ ๊ฐ์ ธ์จ๋ค4* e.g. 11์ด๋ผ๋ ID๋ฅผ ๊ฐ๊ณ ์๋ Post ํ๋๋ฅผ ๊ฐ์ ธ์จ๋ค.5*/6// @Param('id') ๋ป : ๊ฐ์ ธ์ค๋ ํ๋ผ๋ฏธํฐ์ ์ด๋ฆ์ id์ด๋ค7@Get(':id')8getPost(@Param('id', ParseIntPipe) id: number) {9return this.postsService.getPostById(id)10}1112// ์๋ต1314/*** 4) PATCH /posts/:id15* id์ ํด๋นํ๋ post๋ฅผ ๋ถ๋ถ ๋ณ๊ฒฝํ๋ค16*/17@Patch(':id')18putPost(19@Param('id', ParseIntPipe) id: number, //20@Body('title') title?: string,21@Body('content') content?: string,22) {23return this.postsService.updatePost(id, title, content)24}2526/*** 5) DELETE /posts/:id27* id์ ํด๋นํ๋ post๋ฅผ ์ญ์ ํ๋ค28*/29@Delete(':id')30deletePost(@Param('id', ParseIntPipe) id: number) {31return this.postsService.deletePost(id)32}
๊ธฐ์กด์ id๋ฅผ ๋ฌธ์์ด๋ก ๋ฐ์, ์ซ์๋ก ๋ณํํ๊ธฐ ์ํด +๋ฅผ ๋ถ์ธ ๊ฑธ ์์ ๊ฐ์ด ์์ ํ ์ ์๋ค. ๊ทธ๋ฆฌ๊ณ ์๋ฌ๋ Pipe๊ฐ ์๋์ผ๋ก ๋์ ธ์ค๋ค.
1.7 Custom Pipe
1import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'23@Injectable()4export class ParseIntPipe implements PipeTransform<string, number> {5transform(value: string, metadata: ArgumentMetadata): number {6const val = parseInt(value, 10)7if (isNaN(val)) {8throw new BadRequestException('Validation failed. "${value}" is not a valid number.')9}10return val11}12}
2. custom Pipe ๋ง๋ค๊ธฐ
๋น๋ฐ๋ฒํธ๊ฐ 8์๋ฆฌ ์ดํ์ฌ์ผ๋ง ํ๋ค๊ณ ๊ฐ์ ํ๋ค๋ฉด?
- 8์๋ฆฌ ์ด๊ณผ๋๋ฉด, ์๋ฌ๋๊ฒ ๋ง๋ค๊ธฐ ์ํด, ์ปค์คํ pipe ๋ง๋ค์ด๋ณด์
src/auth/pipe/password.pipe.tsํ์ผ์ ๋ง๋ ๋ค.- ์ปค์คํ ํ์ดํ๋ฅผ ๋ง๋ค๊ธฐ ์ํด import ํ๋ ๊ฑด ๊ทธ๋ฅ ์ธ์ฐ๋ฉด ๋๋ค.
1import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'23@Injectable()4export class PasswordPipe implements PipeTransform {5transform(value: any, metadata: ArgumentMetadata) {6if (value.toString().length > 8) {7throw new BadRequestException('๋น๋ฐ๋ฒํธ๋ 8์ ์ดํ๋ก ์ ๋ ฅํด์ฃผ์ธ์!')8}9return value.toString()10}11}
๊ทธ๋ฆฌ๊ณ ๋ง๋ ์ปค์คํ Pipe๋ฅผ ์ฐ๋ ค๊ณ ํ๋ ๊ณณ(auth ์ปจํธ๋กค๋ฌ)์ ํ์ดํ ์ฐ๋ ๊ฒ์ฒ๋ผ ์ฐ๋ฉด ๋๋ค.
1// auth.controller.ts ์๋ต2@Post('register/email')3postRegisterEmail(4@Body('nickname') nickname: string, //5@Body('email') email: string,6@Body('password', PasswordPipe) password: string,7) {8return this.authService.registerWithEmail({9nickname,10email,11password,12})13}
3. DefaultValuePipe ์ฌ์ฉ
1// posts.controller.ts ์๋ต2/*** 3) POST /posts3* post๋ฅผ ์์ฑํ๋ค4*/5@Post()6postPosts(7@Body('authorId') authorId: number, //8@Body('title') title: string,9@Body('content') content: string,10// ๊ธฐ๋ณธ๊ฐ์ true๋ก ์ค์ ํ๋ ํ์ดํ11@Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,12) {13return this.postsService.createPost(authorId, title, content)14}
new๋ก ์ธ์คํด์คํ๋ฅผ ํด์ฃผ๋ฉด, ํด๋น ํจ์๊ฐ ์คํ๋ ๋๋ง๋ค DefaultValuePipe๊ฐ ์๋ก ์์ฑ๋๋ค. ๋ค๋ฅธ Pipe์์๋ new ์ธ์คํด์คํ๋ฅผ ํด์ค ์๋ ์๋ค.
4. ์ฌ๋ฌ ๊ฐ์ ํ์ดํ ๋์์ ์ฌ์ฉ
๋ฐ๋ก ์์์ DefaultValuePipe์์ ํ ์คํธ์ฉ์ผ๋ก ํ์ธํ isPublic์ ์ง์์ฃผ๊ฑฐ๋ ์ฃผ์์ฒ๋ฆฌํ๋ค.
password.pipe.ts์ ์ต์, ์ต๋๊ธธ์ด ์ปค์คํ
ํ์ดํ๋ฅผ ๋ง๋ ๋ค.
1import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'23@Injectable()4export class PasswordPipe implements PipeTransform {5transform(value: any, metadata: ArgumentMetadata) {6if (value.toString().length > 8) {7throw new BadRequestException('๋น๋ฐ๋ฒํธ๋ 8์ ์ดํ๋ก ์ ๋ ฅํด์ฃผ์ธ์!')8}9return value.toString()10}11}1213@Injectable()14export class MaxLengthPipe implements PipeTransform {15constructor(private readonly length: number, private readonly subject: string) {}1617transform(value: any, metadata: ArgumentMetadata) {18if (value.toString().length > this.length) {19throw new BadRequestException(`${this.subject}์ ์ต๋ ๊ธธ์ด๋ ${this.length}์ ๋๋ค.`)20}21return value.toString()22}23}2425@Injectable()26export class MinLengthPipe implements PipeTransform {27constructor(private readonly length: number) {}2829transform(value: any, metadata: ArgumentMetadata) {30if (value.toString().length < this.length) {31throw new BadRequestException(`์ต์ ๊ธธ์ด๋ ${this.length}์ ๋๋ค.`)32}33return value.toString()34}35}
์ธ์คํด์คํ๋ฅผ ํด์ ๋ฃ๊ฒ ๋๋ฉด, constructor๊ฐ์ ๋ฃ์ด์ค ์๊ฐ ์๋ ์ฅ์ ์ด ์๋ค.
1@Post('register/email')2postRegisterEmail(3@Body('nickname') nickname: string, //4@Body('email') email: string,5@Body('password', new MaxLengthPipe(8, '๋น๋ฐ๋ฒํธ'), new MinLengthPipe(3)) password: string,6) {7return this.authService.registerWithEmail({8nickname,9email,10password,11})12}
์ฌ๋ฌ ๊ฐ์ ํ์ดํ๋ฅผ ๋ฃ๊ณ ์ถ์ผ๋ฉด, 2๋ฒ์จฐ ํ๋ผ๋ฏธํฐ๋ถํฐ ๊ณ์ ๋ฃ์ผ๋ฉด, ๊ณ์ ์ฐ๋ฌ์์ ํ์ดํ๊ฐ ์ ์ฉ๋๋ค.