๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
12-Pipe

1. Pipe ์†Œ๊ฐœ & ParseIntPipe

Pipe

  1. Request
  2. Middleware
    • Global Middleware
    • Module Middleware
  3. Guard
    • Global Guards
    • Controller Guards
    • Route Guards
  4. Interceptors
    • Global Interceptors
    • Controller Interceptors
    • Route Interceptors
  5. Pipes
    • Global Pipes
    • Controller Pipes
    • Route Pipes
    • Route Parameter Pipes
  6. Controller
  7. Service
  8. Interceptor
    • Router Interceptor
    • Controller Interceptor
    • Global Interceptor
  9. Exception Filters
    • Route
    • Controller
    • Global
  10. Response

1.1 Pipe๋ž€?

  • Pipe๋ž€? @injectable ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋œ ํด๋ž˜์Šค๋‹ค.
    • PipeTransform ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†(implements)๋ฐ›๋Š”๋‹ค.
    • cf. https://docs.nestjs.com/pipes
  • ํŒŒ์ดํ”„๋Š” Controller์— ์ œ๊ณต๋˜๋Š” argument๋“ค์— ์ ์šฉ๋œ๋‹ค.
    • ์ด argument์—๋Š” @Body, @Param๋“ฑ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์ž…๋ ฅ๋ฐ›๋Š” Annotation๋“ค์ด ๋ชจ๋‘ ํฌํ•จ๋œ๋‹ค.
    • Pipe๋Š” argument ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•œ ํ›„ Controller ๋ฉ”์„œ๋“œ๋กœ ๊ฐ’๋“ค์„ ๋„˜๊ฒจ์ค€๋‹ค

ํŒŒ์ดํ”„๋Š” 2๊ฐ€์ง€ ์‚ฌ์šฉ ์‚ฌ๋ก€๊ฐ€ ์žˆ๋‹ค.

  • transformation(๋ณ€ํ˜•) : ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ์›ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜(e.g. ๋ฌธ์ž์—ด์—์„œ ์ •์ˆ˜๋กœ)
  • validation(๊ฒ€์ฆ) : ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ํ‰๊ฐ€ํ•˜๊ณ  ์œ ํšจํ•œ ๊ฒฝ์šฐ, ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•œ๋‹ค.
    • ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์˜ˆ์™ธ๋ฅผ throwํ•œ๋‹ค.

1.2 Global Pipe

1
import { ValidationPipe } from '@nestjs/common'
2
import { NestFactory } from '@nestjs/core'
3
import { AppModule } from './app.module'
4
5
async function bootstrap() {
6
const app = await NestFactory.create(AppModule)
7
app.useGlobalPipes(new ValidationPipe())
8
await app.listen(3000)
9
}
10
bootstrap()

1.3 Controller Pipe

1
@Controller('movie')
2
@UsePipes(new ValidationPipe())
3
export class MovieController {
4
constructor(private readonly movieService: MovieService) {}
5
6
@Get()
7
getMovies(@Query('title') title?: string) {
8
return this.movieService.findAll(title)
9
}
10
}

1.4 Route Pipe

1
@Controller('movie')
2
export class MovieController {
3
constructor(private readonly movieService: MovieService) {}
4
5
@Patch(':id')
6
@UsePipes(new ValidationPipe())
7
patchMovie(@Param('id') id: string, @Body() body: UpdateMovieDto) {
8
return this.movieService.update(+id, body)
9
}
10
}

1.5 Route Parameter Pipe

1
@Controller('movie')
2
export class MovieController {
3
constructor(private readonly movieService: MovieService) {}
4
5
@Patch(':id')
6
patchMovie(@Param('id', ParseIntPipe) id: number, @Body() body: UpdateMovieDto) {
7
return 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๋ฒˆ์จฐ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋‹ค๊ฐ€ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ํ˜•ํƒœ๋ฅผ ๋„ฃ์œผ๋ฉด ๋œ๋‹ค.

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
/*** 2) GET /posts/:id
3
* id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค
4
* e.g. 11์ด๋ผ๋Š” ID๋ฅผ ๊ฐ–๊ณ ์žˆ๋Š” Post ํ•˜๋‚˜๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
5
*/
6
// @Param('id') ๋œป : ๊ฐ€์ ธ์˜ค๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ด๋ฆ„์€ id์ด๋‹ค
7
@Get(':id')
8
getPost(@Param('id', ParseIntPipe) id: number) {
9
return this.postsService.getPostById(id)
10
}
11
12
// ์ƒ๋žต
13
14
/*** 4) PATCH /posts/:id
15
* id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ๋ถ€๋ถ„ ๋ณ€๊ฒฝํ•œ๋‹ค
16
*/
17
@Patch(':id')
18
putPost(
19
@Param('id', ParseIntPipe) id: number, //
20
@Body('title') title?: string,
21
@Body('content') content?: string,
22
) {
23
return this.postsService.updatePost(id, title, content)
24
}
25
26
/*** 5) DELETE /posts/:id
27
* id์— ํ•ด๋‹นํ•˜๋Š” post๋ฅผ ์‚ญ์ œํ•œ๋‹ค
28
*/
29
@Delete(':id')
30
deletePost(@Param('id', ParseIntPipe) id: number) {
31
return this.postsService.deletePost(id)
32
}

๊ธฐ์กด์— id๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ›์•„, ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด +๋ฅผ ๋ถ™์ธ ๊ฑธ ์œ„์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์—๋Ÿฌ๋„ Pipe๊ฐ€ ์ž๋™์œผ๋กœ ๋˜์ ธ์ค€๋‹ค.


1.7 Custom Pipe

1
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
2
3
@Injectable()
4
export class ParseIntPipe implements PipeTransform<string, number> {
5
transform(value: string, metadata: ArgumentMetadata): number {
6
const val = parseInt(value, 10)
7
if (isNaN(val)) {
8
throw new BadRequestException('Validation failed. "${value}" is not a valid number.')
9
}
10
return val
11
}
12
}

2. custom Pipe ๋งŒ๋“ค๊ธฐ

๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ 8์ž๋ฆฌ ์ดํ•˜์—ฌ์•ผ๋งŒ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค๋ฉด?

  • 8์ž๋ฆฌ ์ดˆ๊ณผ๋˜๋ฉด, ์—๋Ÿฌ๋‚˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด, ์ปค์Šคํ…€ pipe ๋งŒ๋“ค์–ด๋ณด์ž
  • src/auth/pipe/password.pipe.tsํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.
  • ์ปค์Šคํ…€ ํŒŒ์ดํ”„๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด import ํ•˜๋Š” ๊ฑด ๊ทธ๋ƒฅ ์™ธ์šฐ๋ฉด ๋œ๋‹ค.
password.pipe.ts
1
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'
2
3
@Injectable()
4
export class PasswordPipe implements PipeTransform {
5
transform(value: any, metadata: ArgumentMetadata) {
6
if (value.toString().length > 8) {
7
throw new BadRequestException('๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!')
8
}
9
return value.toString()
10
}
11
}

๊ทธ๋ฆฌ๊ณ  ๋งŒ๋“  ์ปค์Šคํ…€ Pipe๋ฅผ ์“ฐ๋ ค๊ณ  ํ•˜๋Š” ๊ณณ(auth ์ปจํŠธ๋กค๋Ÿฌ)์— ํŒŒ์ดํ”„ ์“ฐ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์“ฐ๋ฉด ๋œ๋‹ค.

auth.controller.ts
1
// auth.controller.ts ์ƒ๋žต
2
@Post('register/email')
3
postRegisterEmail(
4
@Body('nickname') nickname: string, //
5
@Body('email') email: string,
6
@Body('password', PasswordPipe) password: string,
7
) {
8
return this.authService.registerWithEmail({
9
nickname,
10
email,
11
password,
12
})
13
}

3. DefaultValuePipe ์‚ฌ์šฉ

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
/*** 3) POST /posts
3
* post๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
4
*/
5
@Post()
6
postPosts(
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
) {
13
return this.postsService.createPost(authorId, title, content)
14
}

new๋กœ ์ธ์Šคํ„ด์Šคํ™”๋ฅผ ํ•ด์ฃผ๋ฉด, ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋  ๋–„๋งˆ๋‹ค DefaultValuePipe๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋œ๋‹ค. ๋‹ค๋ฅธ Pipe์—์„œ๋„ new ์ธ์Šคํ„ด์Šคํ™”๋ฅผ ํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.


4. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ดํ”„ ๋™์‹œ์— ์‚ฌ์šฉ

๋ฐ”๋กœ ์œ„์—์„œ DefaultValuePipe์—์„œ ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ™•์ธํ•œ isPublic์€ ์ง€์›Œ์ฃผ๊ฑฐ๋‚˜ ์ฃผ์„์ฒ˜๋ฆฌํ•œ๋‹ค.

password.pipe.ts์— ์ตœ์†Œ, ์ตœ๋Œ€๊ธธ์ด ์ปค์Šคํ…€ ํŒŒ์ดํ”„๋ฅผ ๋งŒ๋“ ๋‹ค.

password.pipe.ts
1
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'
2
3
@Injectable()
4
export class PasswordPipe implements PipeTransform {
5
transform(value: any, metadata: ArgumentMetadata) {
6
if (value.toString().length > 8) {
7
throw new BadRequestException('๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!')
8
}
9
return value.toString()
10
}
11
}
12
13
@Injectable()
14
export class MaxLengthPipe implements PipeTransform {
15
constructor(private readonly length: number, private readonly subject: string) {}
16
17
transform(value: any, metadata: ArgumentMetadata) {
18
if (value.toString().length > this.length) {
19
throw new BadRequestException(`${this.subject}์˜ ์ตœ๋Œ€ ๊ธธ์ด๋Š” ${this.length}์ž…๋‹ˆ๋‹ค.`)
20
}
21
return value.toString()
22
}
23
}
24
25
@Injectable()
26
export class MinLengthPipe implements PipeTransform {
27
constructor(private readonly length: number) {}
28
29
transform(value: any, metadata: ArgumentMetadata) {
30
if (value.toString().length < this.length) {
31
throw new BadRequestException(`์ตœ์†Œ ๊ธธ์ด๋Š” ${this.length}์ž…๋‹ˆ๋‹ค.`)
32
}
33
return value.toString()
34
}
35
}

์ธ์Šคํ„ด์Šคํ™”๋ฅผ ํ•ด์„œ ๋„ฃ๊ฒŒ ๋˜๋ฉด, constructor๊ฐ’์„ ๋„ฃ์–ด์ค„ ์ˆ˜๊ฐ€ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

auth.controller.ts
1
@Post('register/email')
2
postRegisterEmail(
3
@Body('nickname') nickname: string, //
4
@Body('email') email: string,
5
@Body('password', new MaxLengthPipe(8, '๋น„๋ฐ€๋ฒˆํ˜ธ'), new MinLengthPipe(3)) password: string,
6
) {
7
return this.authService.registerWithEmail({
8
nickname,
9
email,
10
password,
11
})
12
}

์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ดํ”„๋ฅผ ๋„ฃ๊ณ  ์‹ถ์œผ๋ฉด, 2๋ฒˆ์จฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋ถ€ํ„ฐ ๊ณ„์† ๋„ฃ์œผ๋ฉด, ๊ณ„์† ์—ฐ๋‹ฌ์•„์„œ ํŒŒ์ดํ”„๊ฐ€ ์ ์šฉ๋œ๋‹ค.