IsDecimal์€ ์™œ ์ˆซ์ž๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ• ๊นŒ?

IsDecimal์€ ์™œ ์ˆซ์ž๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ• ๊นŒ?

D
dongAuthor
4 min read

NestJS์—์„œ class-validator๋Š” ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์•„์ฃผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. DTO(Data Transfer Object)์— ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ช‡ ๊ฐœ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด, ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ์˜ ํ˜•์‹์„ ์†์‰ฝ๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์ฃ . ํ•˜์ง€๋งŒ ๊ฐ€๋” ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋•Œ๋ฌธ์— ๊ณจ๋จธ๋ฆฌ๋ฅผ ์•“๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ @IsDecimal์ž…๋‹ˆ๋‹ค.

๋ถ„๋ช…ํžˆ ์†Œ์ˆ˜์  ํ˜•ํƒœ์˜ ์ˆซ์ž๋ฅผ ๋ณด๋ƒˆ๋Š”๋ฐ๋„ "์œ ํšจํ•œ 10์ง„์ˆ˜๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"๋ผ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ƒํ™ฉ, ๊ฒช์–ด๋ณด์…จ๋‚˜์š”? ์ด ๊ธ€์—์„œ๋Š” @IsDecimal์ด ์™œ ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ค๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  NestJS์—์„œ ์†Œ์ˆ˜์  ์ˆซ์ž๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ธ์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

@IsDecimal์˜ ์˜คํ•ด์™€ ์ง„์‹ค

๋งŽ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ @IsDecimal์„ ์ˆซ์ž ํƒ€์ž…์˜ ์†Œ์ˆ˜์  ๊ฐ’์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Stack Overflow์˜ ์—ฌ๋Ÿฌ ๋…ผ์˜์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ, @IsDecimal์€ ๋ฌธ์ž์—ด(string) ํƒ€์ž…์˜ ๊ฐ’์ด 10์ง„์ˆ˜ ํ˜•์‹์— ๋งž๋Š”์ง€๋งŒ ํ™•์ธํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ class-validator์˜ GitHub ์ด์Šˆ(#1423)๋ฅผ ๋ณด๋ฉด, @IsDecimal์˜ ์„ค๋ช… ์ฃผ์„์ด ์ž˜๋ชป๋˜์–ด ํ˜ผ๋ž€์„ ์•ผ๊ธฐํ–ˆ๋˜ ๊ณผ๊ฑฐ ๊ธฐ๋ก๋„ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ์ˆ˜์ •๋˜์—ˆ์ง€๋งŒ, ์—ฌ์ „ํžˆ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ด ๋ถ€๋ถ„์„ ํ—ท๊ฐˆ๋ ค ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ•œ๋ฒˆ ๋ณด์‹œ์ฃ .

// create-product.dto.ts

import { IsDecimal, IsNotEmpty, Min } from 'class-validator';

export class CreateProductDto {
  @IsDecimal({ decimal_digits: '2' }) // ๐Ÿšจ ์ด ๋ถ€๋ถ„์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค!
  @IsNotEmpty()
  @Min(0)
  price: number; // ํƒ€์ž…์ด 'number'์ž…๋‹ˆ๋‹ค.
}

์ด DTO์— JSON ํ˜•ํƒœ๋กœ { "price": 1574.23 }๊ณผ ๊ฐ™์€ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, class-validator๋Š” price ํ•„๋“œ๊ฐ€ ์ˆซ์ž ํƒ€์ž…์ด๊ธฐ ๋•Œ๋ฌธ์— @IsDecimal ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค์ง€ ๋ชปํ•˜๊ณ  ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์ˆซ์ž ํƒ€์ž…์˜ ์†Œ์ˆ˜์ ์€ ์–ด๋–ป๊ฒŒ ๊ฒ€์ฆํ•ด์•ผ ํ• ๊นŒ์š”? ๋‘ ๊ฐ€์ง€ ํ•ด๊ฒฐ์ฑ…์ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ… 1: @IsNumber ์‚ฌ์šฉํ•˜๊ธฐ

๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ธ ํ•ด๊ฒฐ์ฑ…์€ @IsNumber ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. @IsNumber๋Š” ์ˆซ์ž ํƒ€์ž…์˜ ๊ฐ’์„ ๊ฒ€์ฆํ•˜๋ฉฐ, maxDecimalPlaces ์˜ต์…˜์„ ํ†ตํ•ด ํ—ˆ์šฉํ•  ์ตœ๋Œ€ ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// create-product.dto.ts (์ˆ˜์ • ํ›„)

import { IsNumber, IsNotEmpty, Min } from 'class-validator';

export class CreateProductDto {
  @IsNumber({ maxDecimalPlaces: 2 }) // โœ… ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”!
  @IsNotEmpty()
  @Min(0)
  price: number;
}

์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๋ฉด price ํ•„๋“œ๊ฐ€ ์ˆซ์ž์ด๋ฉด์„œ ์†Œ์ˆ˜์  ์ดํ•˜ ๋‘ ์ž๋ฆฌ๊นŒ์ง€๋งŒ ํ—ˆ์šฉํ•˜๋„๋ก ์ •ํ™•ํ•˜๊ฒŒ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ •๋ฐ€๋„๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด, TypeORM ๊ฐ™์€ ORM์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์—”ํ‹ฐํ‹ฐ(Entity) ํŒŒ์ผ์—์„œ @Column('decimal') ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ์ •์˜ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ… 2: ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋งŒ๋“ค๊ธฐ

์กฐ๊ธˆ ๋” ๋ณต์žกํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. exonerate์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ทœ์น™์„ ์กฐํ•ฉํ•˜์—ฌ ๊ฐ•๋ ฅํ•œ ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ €, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด ์ฃผ์„ธ์š”.

npm install exonerate

๊ทธ๋‹ค์Œ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด DTO์—์„œ @Exonerate ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ๊ทœ์น™์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// create-user.dto.ts (exonerate ์‚ฌ์šฉ ์˜ˆ์‹œ)

import { Exonerate } from 'exonerate';
import { User } from './user.entity'; // User ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
import { AddressDto } from './address.dto'; // Address DTO๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •

enum ROLE {
  ADMIN = 'ADMIN',
  USER = 'USER',
}

export class CreateUserDto {
    @Exonerate({ rules: 'required|string|max:20|min:4|exist:name', entity: User })
    name: string;

    @Exonerate({ rules: 'required|email|unique:email', entity: User })
    email: string;

    @Exonerate({
        rules: 'required|max:20|min:8|pattern',
        regexPattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/
    })
    password: string;

    @Exonerate({ rules: 'required|enum', enumType: ROLE })
    role: string;
}

exonerate๋Š” ์†Œ์ˆ˜์  ๊ฒ€์ฆ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ(unique:email), ํŠน์ • ํŒจํ„ด ๊ฒ€์‚ฌ(pattern) ๋“ฑ ๋ณตํ•ฉ์ ์ธ ๊ทœ์น™์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ค๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์— ๊ฑธ์ณ ์ผ๊ด€๋˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์ด ํ•„์š”ํ•  ๋•Œ ์•„์ฃผ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ „์—ญ ํŒŒ์ดํ”„ ์„ค์ •ํ•˜๊ธฐ

class-validator๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์—ญ์—์„œ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด main.ts ํŒŒ์ผ์— ValidationPipe๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์„ค์ •์„ ์žŠ์ง€ ๋งˆ์„ธ์š”!

// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // โœ… ์ „์—ญ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํŒŒ์ดํ”„๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  app.useGlobalPipes(new ValidationPipe());
  
  await app.listen(3000);
}
bootstrap();

๋˜ํ•œ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด app.module.ts์— ConfigModule ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋„ ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.

// app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    // ... ๋‹ค๋ฅธ ๋ชจ๋“ˆ๋“ค
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

์˜ฌ๋ฐ”๋ฅธ ๋„๊ตฌ๋ฅผ ์˜ฌ๋ฐ”๋ฅธ ๊ณณ์—

์ด๋ฒˆ ์—ฌ์ •์„ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” ์ค‘์š”ํ•œ ๊ตํ›ˆ์„ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค. @IsDecimal์€ ๋ฌธ์ž์—ด์„ ์œ„ํ•œ ๋„๊ตฌ์ด๋ฉฐ, ์ˆซ์ž ํƒ€์ž…์˜ ์†Œ์ˆ˜์  ๊ฐ’์„ ๊ฒ€์ฆํ•  ๋•Œ๋Š” @IsNumber๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋‚˜ ํ•จ์ˆ˜์˜ ์„ค๋ช…์„ ๊ผผ๊ผผํžˆ ํ™•์ธํ•˜๋Š” ์Šต๊ด€์€ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๋ฅผ ๋ง‰์•„์ฃผ๋Š” ํ›Œ๋ฅญํ•œ ๋ฐฉํŒจ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๊ณต์‹ ๋ฌธ์„œ์˜ ์„ค๋ช…์ด ๋ถ€์กฑํ•˜๊ฑฐ๋‚˜ ํ˜ผ๋ž€์Šค๋Ÿฝ๋‹ค๋ฉด, ์ฃผ์ €ํ•˜์ง€ ๋ง๊ณ  GitHub ์ด์Šˆ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜์—ฌ ๋ช…ํ™•ํžˆ ์ดํ•ดํ•˜๊ณ  ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์ฃ  !

References

IsDecimal์€ ์™œ ์ˆซ์ž๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ• ๊นŒ?