Nestjs์์ class validator IsEnum ์ ๋๋ก ์จ๋ณด์
NestJS์์ DTO ๊ฒ์ฆ์ ํ๋ค ๋ณด๋ฉด โํน์ ๊ฐ๋ง ํ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐโ๊ฐ ๋ง์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ญํ (role)์ 'admin' | 'user' | 'blogger' ์ค ํ๋๋ง, ์ํ ์ฌ์ด์ฆ(size)๋ 'S' | 'M' | 'L' | 'XL' ์ค ํ๋๋ง ํ์ฉํ๊ณ ์ถ์ ๋์ฃ . ์ด๋ด ๋ class-validator์ @IsEnum ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ ๋๋ก ํ์ฉํ๋ฉด ๊น๋ํ๊ณ ์์ ํ ์
๋ ฅ ๊ฒ์ฆ์ ๊ตฌํํ ์ ์์ต๋๋ค.
@IsEnum์ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ ๋ณต์ต
// enums/user-role.enum.ts
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
BLOGGER = 'blogger',
}
// dto/create-user.dto.ts
import { IsEnum, IsString, IsNotEmpty, IsEmail } from 'class-validator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@IsEnum(UserRole, {
message: 'role must be one of: admin, user, blogger',
})
role: UserRole;
}
@IsEnum์ ํ์
์์ ์ฑ๊ณผ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋์์ ๋ณด์ฅํฉ๋๋ค. ์ฆ, role ๊ฐ์ด enum์ ํฌํจ๋์ง ์์ผ๋ฉด ์๋์ผ๋ก 400 ์๋ฌ๋ฅผ ๋ฐ์์ํต๋๋ค.
๋์๋ฌธ์ ๊ตฌ๋ถ ์๋ ๊ฒ์ฆ (Case-insensitive Validation)
ํ์ค์์๋ ์ฌ์ฉ์๊ฐ "Admin", "ADMIN", "admin" ๋ฑ ๋ค์ํ ์
๋ ฅ์ ๋ณด๋
๋๋ค. ์ด๋ฅผ ํ์ฉํ์ง ์์ผ๋ฉด ๋ถํ์ํ ์ฌ์ฉ์ ์ค๋ฅ๋ฅผ ์ ๋ฐํ์ฃ . ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ์ธ ๊ฐ์ง ํจํด์ ์๊ฐํฉ๋๋ค ๐
1๏ธโฃ @Transform์ผ๋ก ๋ฏธ๋ฆฌ ์๋ฌธ์ ๋ณํํ๊ธฐ
๊ฐ์ฅ ๊ฐ๋จํ๋ฉด์ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ๋๋ค.
import { IsEnum, IsString, IsNotEmpty } from 'class-validator';
import { Transform } from 'class-transformer';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@Transform(({ value }) => value?.toString().toLowerCase())
@IsEnum(UserRole, {
message: 'role must be one of: admin, user, blogger',
})
role: UserRole;
}
๐ก ์ฅ์ : ๊ตฌํ ๊ฐ๋จ, ๋ฐํ์ ์ค๋ฒํค๋ ๊ฑฐ์ ์์
โ ๏ธ ๋จ์ : enum ๊ฐ์ด ๋ฐ๋์ ์๋ฌธ์๋ก ์ ์๋์ด ์์ด์ผ ํจ
Tip: enum ๊ฐ์ ๋ชจ๋ ์๋ฌธ์๋ก ์ ์ํ๊ณ , ์
๋ ฅ๊ฐ์ .toLowerCase()๋ก ๋ณํํ๋ฉด REST API ์์ฒญ ์ผ์ด์ค ์ฐจ์ด๋ก ์ธํ ์ค๋ฅ๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
2๏ธโฃ @Matches๋ฅผ ํตํ ์ ๊ท์ ๊ธฐ๋ฐ ๊ฒ์ฆ
enum ๊ฐ์ด ๋ง๊ฑฐ๋ ๋์ ์ผ๋ก ๋ณ๊ฒฝ๋ ๋ ์ ์ฉํฉ๋๋ค.
import { Matches, IsString } from 'class-validator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@IsString()
@Matches(`^(${Object.values(UserRole).join('|')})$`, 'i', {
message: 'role must be one of: admin, user, blogger (case-insensitive)',
})
role: UserRole;
}
๐ก ์ฅ์ : ๋์๋ฌธ์ ๋ฌด์(
iํ๋๊ทธ) ๊ฐ๋ฅ
โ ๏ธ ๋จ์ : ์ ๊ท์ ๋ณต์ก์ฑ ์ฆ๊ฐ, enum ํ์ ์๋์์ฑ ๋ถ๊ฐ
3๏ธโฃ ์ปค์คํ
๋ฐ์ฝ๋ ์ดํฐ๋ก IsEnumCaseInsensitive ๋ง๋ค๊ธฐ
์ง์ง โํ์ ์ฉโ ๋ฐฉ์์ ๋๋ค. ๋ฐ๋ณต๋๋ ๋ณํ๊ณผ ์ ๊ท์ ๋์ , ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค์ด๋ด ์๋ค.
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsEnumCaseInsensitiveConstraint implements ValidatorConstraintInterface {
validate(value: any, args: any) {
const [enumObject] = args.constraints;
const enumValues = Object.values(enumObject).map(v => v.toString().toLowerCase());
return enumValues.includes(value?.toString().toLowerCase());
}
defaultMessage() {
return '์
๋ ฅ๊ฐ์ด ํ์ฉ๋ enum ๋ชฉ๋ก์ ์์ต๋๋ค.';
}
}
export function IsEnumCaseInsensitive(enumObject: any, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [enumObject],
validator: IsEnumCaseInsensitiveConstraint,
});
};
}
์ฌ์ฉ ์์ ๐
import { Transform } from 'class-transformer';
import { IsEnumCaseInsensitive } from '../validators/is-enum-case-insensitive.decorator';
import { UserRole } from '../enums/user-role.enum';
export class CreateUserDto {
@Transform(({ value }) => value?.toString().toLowerCase())
@IsEnumCaseInsensitive(UserRole, {
message: 'role์ admin, user, blogger ์ค ํ๋์ฌ์ผ ํฉ๋๋ค (๋์๋ฌธ์ ๋ฌด๊ด)',
})
role: UserRole;
}
๐ก ์ฅ์ : enum๋ณ๋ก ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
โ๏ธ ๋จ์ : ์ฝ๊ฐ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์กด์ฌ
ํ์
ํ: ๊ณต์ฉ ์ ํจ์ฑ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ src/validators/ ๊ฒฝ๋ก์ ๋๊ณ , ๋ชจ๋ DTO์์ ๋ถ๋ฌ ์ฐ๋ฉด ์ ์ง๋ณด์๊ฐ ํจ์ฌ ํธํด์ง๋๋ค.
์ค๋ฌด ํ: transform: true ์ต์
์ ๊ผญ ํ์ฑํํ๊ธฐ
NestJS์ main.ts์์ ValidationPipe๋ฅผ ์ค์ ํ ๋ ๋ฐ๋์ transform: true๋ฅผ ์ผ์ผ ํฉ๋๋ค.
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
์ด ์ค์ ์ด ์์ด์ผ @Transform()์ด ์ ์ฉ๋๊ณ ๋ฌธ์์ด โ enum ๋ณํ์ด ์ ์ ๋์ํฉ๋๋ค.
์ฃผ์: transform์ ๋๋ฉด DTO์ @Transform()๊ณผ @Type()์ด ๋ฌด์๋์ด, enum ๊ฒ์ฆ์ด ํญ์ ์คํจํ๊ฒ ๋ฉ๋๋ค.
๋ค๋ฅธ ์ค์ฉ์ ์ธ ๊ฒ์ฆ ํจํด
@IsEnum ์ธ์๋ class-validator์๋ DTO๋ฅผ ๋ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ์ ์ฉํ ์กฐํฉ๋ค์ด ์์ต๋๋ค ๐
๋ณตํฉ ๋ฌธ์์ด ๊ฒ์ฆ
@IsString()
@MinLength(2)
@MaxLength(30)
@Matches(/^[๊ฐ-ํฃa-zA-Z\s]+$/, {
message: '์ด๋ฆ์ ํ๊ธ, ์๋ฌธ, ๊ณต๋ฐฑ๋ง ํฌํจํ ์ ์์ต๋๋ค.',
})
name: string;
์ค์ฒฉ ๊ฐ์ฒด ๊ฒ์ฆ
import { ValidateNested, Type } from 'class-validator';
class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class UserWithAddressDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
๋ฐฐ์ด ๋ด ๊ฐ์ฒด ๊ฒ์ฆ
import { IsArray, ValidateNested, Type } from 'class-validator';
import { ProductSize } from '../enums/product-size.enum';
class OrderItemDto {
@IsString()
productName: string;
@IsEnum(ProductSize)
size: ProductSize;
}
export class OrderDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
IsEnum vs ํ์ ์์ ์ฑ
TypeScript์ enum์ ์ปดํ์ผ ํ์์ ํ์
์ ๋ณด์ฅํ๊ณ , @IsEnum์ ๋ฐํ์์์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํฉ๋๋ค. ์ฆ, ๋์ ํจ๊ป ์จ์ผ ์ง์ง ์์ ํฉ๋๋ค.
| ๊ตฌ๋ถ | TypeScript Enum | @IsEnum |
|---|---|---|
| ์์ | ์ปดํ์ผ ํ์ | ๋ฐํ์ |
| ์ญํ | ํ์ ์ ํ | ์ค์ ์์ฒญ ๊ฒ์ฆ |
| ์คํจ ์ | ์ปดํ์ผ ์๋ฌ | 400 Bad Request |