IsDecimal์ ์ ์ซ์๋ฅผ ๊ฒ์ฆํ์ง ๋ชปํ ๊น?
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 ์ด์๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ์ฌ ๋ช ํํ ์ดํดํ๊ณ ๋์ด๊ฐ๋ ๊ฒ์ด ์ข๊ฒ ์ฃ !