class-validator์ IsMongoId: MongoDB ObjectId ๊ฒ์ฆ
NestJS๋ก API๋ฅผ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด MongoDB ObjectId ํ์์ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฒ์ฆํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ฃผ ์๊น๋๋ค. ์๋ชป๋ ํ์์ ID๊ฐ ๋ค์ด์ค๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ณ , ์ฌ์ฉ์์๊ฒ๋ ๋ถ์น์ ํ ์๋ฌ ๋ฉ์์ง๊ฐ ์ ๋ฌ๋์ฃ .
class-validator์ @IsMongoId() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ๋ฌธ์ ๋ฅผ ๊ฐ๋จํ๊ฒ ํด๊ฒฐํ ์ ์์ต๋๋ค. DTO์ ๋ฐ์ฝ๋ ์ดํฐ ํ๋๋ง ์ถ๊ฐํ๋ฉด MongoDB ObjectId ๊ฒ์ฆ์ด ์๋์ผ๋ก ์ด๋ฃจ์ด์ง๊ณ , ์๋ชป๋ ์์ฒญ์ ์ปจํธ๋กค๋ฌ์ ๋๋ฌํ๊ธฐ ์ ์ ์ฐจ๋จ๋ฉ๋๋ค.
์ด ๊ธ์์๋ @IsMongoId()์ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ๋ถํฐ ๋ด๋ถ ๊ตฌํ, ์ค์ ํ์ฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ปค์คํ
๊ฒ์ฆ๊น์ง ๋ชจ๋ ๋ค๋ค๋ณผ๊ฒ์.
Class-Validator์ NestJS์ ๋ง๋จ
NestJS๋ TypeScript ๊ธฐ๋ฐ์ ๊ฐ๋ ฅํ ์๋ฒ ํ๋ ์์ํฌ์
๋๋ค. ๊ทธ์ค์์๋ class-validator๋ NestJS ๊ณต์ ๋ฌธ์์์ ๊ถ์ฅํ๋ ๊ฒ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ์ ์ง๊ด์ ์ธ ๋ฌธ๋ฒ์ผ๋ก ์
๋ ฅ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ ์ ์๊ฒ ํด์ค๋๋ค.
class-validator๋ฅผ ๋์
ํ๋ฉด ์๋น์ค ๋ ์ด์ด์์ ์ผ์ผ์ด ๊ฒ์ฆ ๋ก์ง์ ์์ฑํ ํ์๊ฐ ์์ด์ง๋๋ค. DTO์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ๋ง์ผ๋ก ์์ฒญ ๋ฐ์ดํฐ์ ๊ตฌ์กฐ๋ฅผ ์ ์ดํ ์ ์์ฃ . ์ค์ ๋ก ๋ง์ ๊ฐ๋ฐ์๋ค์ด class-validator ๋์
ํ ์ ํ์ ์์ ์ฑ๊ณผ ๊ฐ๋ฐ ํธ์์ฑ์ด ํฌ๊ฒ ํฅ์๋์๋ค๊ณ ์ด์ผ๊ธฐํฉ๋๋ค.
@IsMongoId()๋ ๋ฌด์์ธ๊ฐ?
์ ์์ ๋ชฉ์
@IsMongoId()๋ class-validator์์ ์ ๊ณตํ๋ ๋ด์ฅ ๋ฐ์ฝ๋ ์ดํฐ๋ก, ๋ฌธ์์ด์ด MongoDB ObjectId ํ์(24์ 16์ง์)์ธ์ง ๊ฒ์ฆํฉ๋๋ค.
MongoDB์์ ๊ฐ ๋ฌธ์๋ ๊ณ ์ ํ _id ํ๋๋ฅผ ๊ฐ์ง๋ฉฐ, ์ด๋ 12๋ฐ์ดํธ BSON ํ์
์ ObjectId์
๋๋ค. ๋ฌธ์์ด๋ก ํํํ๋ฉด 24์๋ฆฌ 16์ง์ ํํ๊ฐ ๋๋๋ฐ, @IsMongoId()๋ ๋ฐ๋ก ์ด ํ์์ ๊ฒ์ฆํ๋ ์ญํ ์ ํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ ์์
import { IsMongoId, IsNotEmpty } from 'class-validator';
export class FindUserDto {
@IsNotEmpty()
@IsMongoId()
userId: string;
}
์ ์์ ์์ userId๋ ๋ฐ๋์ ๋น์ด์์ง ์์ MongoDB ObjectId ํ์์ด์ด์ผ ํฉ๋๋ค. ๋ง์ฝ ์๋ชป๋ ํ์์ ID๊ฐ ์ ๋ฌ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ ์๋ต์ด ๋ฐํ๋ฉ๋๋ค:
{
"statusCode": 400,
"message": ["userId must be a mongodb id"],
"error": "Bad Request"
}
validator.js ํ์ฉ
@IsMongoId()์ ๋ด๋ถ ๊ตฌํ์ ์ดํด๋ณด๋ฉด ํฅ๋ฏธ๋ก์ด ์ ์ ๋ฐ๊ฒฌํ ์ ์์ต๋๋ค:
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import isMongoIdValidator from 'validator/lib/isMongoId';
export const IS_MONGO_ID = 'isMongoId';
export function isMongoId(value: unknown): boolean {
return typeof value === 'string' && isMongoIdValidator(value);
}
export function IsMongoId(validationOptions?: ValidationOptions): PropertyDecorator {
return ValidateBy(
{
name: IS_MONGO_ID,
validator: {
validate: (value): boolean => isMongoId(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be a mongodb id',
validationOptions
),
},
},
validationOptions
);
}
ํต์ฌ ํฌ์ธํธ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- validator.js ์์กด:
validator/lib/isMongoId๋ฅผ ์ฌ์ฉํด ์ค์ ๊ฒ์ฆ์ ์ํํฉ๋๋ค. - ํ์
์ ํ:
stringํ์ ๋ง ํ์ฉํฉ๋๋ค. - ์ ๊ท์ ๊ธฐ๋ฐ: 24์ 16์ง์ ํ์์ ์ ๊ท์์ผ๋ก ๊ฒ์ฆํฉ๋๋ค.
- ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅ:
validationOptions๋ฅผ ํตํด ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
NestJS ํ๋ก์ ํธ์ Class-Validator ์ค์ ํ๊ธฐ
์ค์น
๋จผ์ ํ์ํ ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค:
npm i --save class-validator class-transformer
class-transformer๋ plain object๋ฅผ class instance๋ก ๋ณํํ๋ ๋ฐ ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.
ValidationPipe ์ ์ฉ
NestJS์์ class-validator์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ธ์ํ๋ ค๋ฉด ValidationPipe๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ์ ์ญ์ผ๋ก ์ ์ฉํ๊ฑฐ๋ ํน์ ์ปจํธ๋กค๋ฌ/๋ผ์ฐํธ์๋ง ์ ์ฉํ ์ ์์ต๋๋ค.
์ ์ญ ์ ์ฉ (main.ts):
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
await app.listen(3000);
}
bootstrap();
์ปจํธ๋กค๋ฌ ๋ ๋ฒจ ์ ์ฉ:
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UserController {
@Post('find')
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
async findUser(@Body() dto: FindUserDto) {
return this.userService.findUserById(dto.userId);
}
}
์ฃผ์ ์ต์ :
whitelist: DTO์ ์ ์๋์ง ์์ ์์ฑ์ ์๋์ผ๋ก ์ ๊ฑฐํฉ๋๋ค.forbidNonWhitelisted: DTO์ ์๋ ์์ฑ์ด ํฌํจ๋๋ฉด ์์ฒญ์ ๊ฑฐ๋ถํฉ๋๋ค.transform: plain object๋ฅผ DTO ํด๋์ค ์ธ์คํด์ค๋ก ์๋ ๋ณํํฉ๋๋ค.
DTO์์ @IsMongoId() ํ์ฉํ๊ธฐ
์ค์ ์์ : ์ฌ์ฉ์ ์กฐํ API
// user.dto.ts
import { IsMongoId, IsNotEmpty } from 'class-validator';
export class GetUserByIdDto {
@IsNotEmpty({ message: 'userId๋ ํ์์
๋๋ค.' })
@IsMongoId({ message: 'userId๋ ์ ํจํ MongoDB ObjectId์ฌ์ผ ํฉ๋๋ค.' })
userId: string;
}
// user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('find')
async findUser(@Body() dto: GetUserByIdDto) {
console.log(dto.userId);
return this.userService.findUserById(dto.userId);
}
}
์ ํจํ ์์ฒญ:
{
"userId": "507f191e810c19729de860ea"
}
๋ฌดํจํ ์์ฒญ:
{
"userId": "12345"
}
์๋ต:
{
"statusCode": 400,
"message": ["userId๋ ์ ํจํ MongoDB ObjectId์ฌ์ผ ํฉ๋๋ค."],
"error": "Bad Request"
}
์์ฒญ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ
์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ URL ํ๋ผ๋ฏธํฐ์์๋ ๋์ผํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค:
import { Controller, Get, Param } from '@nestjs/common';
import { IsMongoId } from 'class-validator';
export class UserIdParam {
@IsMongoId()
id: string;
}
@Controller('users')
export class UserController {
@Get(':id')
async getUser(@Param() params: UserIdParam) {
return this.userService.findById(params.id);
}
}
ํํ ๋ฌธ์ ์ ํด๊ฒฐ์ฑ
๋น ๋ฌธ์์ด ์ฒ๋ฆฌ
@IsMongoId()๋ ๋น ๋ฌธ์์ด์ ๊ฒ์ฆํ์ง ์์ต๋๋ค. ๋น ๋ฌธ์์ด๋ ๊ฑฐ๋ถํ๋ ค๋ฉด @IsNotEmpty()๋ฅผ ํจ๊ป ์ฌ์ฉํ์ธ์:
export class CreateNewsDto {
@IsNotEmpty()
@IsString()
title: string;
@IsNotEmpty()
@IsString()
content: string;
@IsNotEmpty()
@IsString()
@IsMongoId()
pageId: string;
@IsOptional()
@IsString()
@IsMongoId()
ownerId: string;
}
@IsOptional()์ ์ฌ์ฉํ๋ฉด ํด๋น ํ๋๊ฐ ์๊ฑฐ๋ undefined์ผ ๋๋ ๊ฒ์ฆ์ ๊ฑด๋๋๋๋ค.
์ปค์คํ ์๋ฌ ๋ฉ์์ง
๊ธฐ๋ณธ ์๋ฌ ๋ฉ์์ง๊ฐ ๋ง์์ ๋ค์ง ์๋๋ค๋ฉด ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค:
export class FindUserDto {
@IsMongoId({ message: '์ฌ๋ฐ๋ฅธ ์ฌ์ฉ์ ID ํ์์ด ์๋๋๋ค.' })
userId: string;
}
๋ค๊ตญ์ด ์ง์์ด ํ์ํ๋ค๋ฉด i18n ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ฒฐํฉํด ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
๊ณ ๊ธ ํ์ฉ๋ฒ๊ณผ ์ปค์คํฐ๋ง์ด์ง
๋ค๋ฅธ ๊ฒ์ฆ ๋ฐ์ฝ๋ ์ดํฐ์ ๊ฒฐํฉ
์ฌ๋ฌ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์กฐํฉํด ๋ ์ ๊ตํ ๊ฒ์ฆ ๊ท์น์ ๋ง๋ค ์ ์์ต๋๋ค:
import { IsMongoId, IsOptional, IsArray, ArrayMinSize } from 'class-validator';
export class AssignTasksDto {
@IsArray()
@ArrayMinSize(1)
@IsMongoId({ each: true, message: '๊ฐ ํญ๋ชฉ์ ์ ํจํ MongoDB ObjectId์ฌ์ผ ํฉ๋๋ค.' })
userIds: string[];
@IsMongoId()
projectId: string;
}
{ each: true } ์ต์
์ ์ฌ์ฉํ๋ฉด ๋ฐฐ์ด์ ๊ฐ ์์๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ๊ฒ์ฆํฉ๋๋ค.
์ปค์คํ ๊ฒ์ฆ๊ธฐ: DB ์กด์ฌ ์ฌ๋ถ ํ์ธ
@IsMongoId()๋ ํ์๋ง ๊ฒ์ฆํ ๋ฟ, ํด๋น ID๊ฐ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ๋์ง๋ ํ์ธํ์ง ์์ต๋๋ค. ์ด๋ฅผ ์ํ ์ปค์คํ
๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค์ด๋ณผ๊น์?
import { registerDecorator, ValidationOptions, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ValidatorConstraint({ name: 'IsExistingMongoId', async: true })
@Injectable()
export class IsExistingMongoIdConstraint implements ValidatorConstraintInterface {
constructor(
@InjectModel('User') private readonly userModel: Model<any>,
) {}
async validate(value: string, args: ValidationArguments): Promise<boolean> {
const modelName = args.constraints[0];
if (modelName === 'User') {
const user = await this.userModel.findById(value).exec();
return !!user;
}
return false;
}
defaultMessage(args: ValidationArguments): string {
return `${args.property}์ ํด๋นํ๋ ๋ฌธ์๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.`;
}
}
export function IsExistingMongoId(
modelName: string,
validationOptions?: ValidationOptions,
): PropertyDecorator {
return (object, propertyName) => {
registerDecorator({
name: 'IsExistingMongoId',
target: object.constructor,
propertyName,
constraints: [modelName],
options: validationOptions,
validator: IsExistingMongoIdConstraint,
});
};
}
์ฌ์ฉ๋ฒ:
export class UpdateUserDto {
@IsExistingMongoId('User', { message: '์กด์ฌํ์ง ์๋ ์ฌ์ฉ์์
๋๋ค.' })
userId: string;
}
์ปค์คํ ๊ฒ์ฆ๊ธฐ๋ ๋น๋๊ธฐ ์์ ์ ์ง์ํ๋ฏ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ๋ฅผ ํตํ ๊ฒ์ฆ์ด ๊ฐ๋ฅํฉ๋๋ค. ๋ค๋ง ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ผ๋ ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ์ธ์.
@IsMongoId()๋ก ๋ ์์ ํ API ๋ง๋ค๊ธฐ
@IsMongoId()๋ ์์ง๋ง ๊ฐ๋ ฅํ ๋๊ตฌ์
๋๋ค. ์ด ๋ฐ์ฝ๋ ์ดํฐ ํ๋๋ก ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์ป์ ์ ์์ต๋๋ค:
- ์ฝ๋ ๊ฐ์ํ: ์๋น์ค ๋ ์ด์ด์์ ๋ฐ๋ณต๋๋ ๊ฒ์ฆ ๋ก์ง์ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
- ์ผ๊ด์ฑ: ๋ชจ๋ MongoDB ObjectId ๊ฒ์ฆ์ด ๋์ผํ ๋ฐฉ์์ผ๋ก ์ด๋ฃจ์ด์ง๋๋ค.
- ์๋ฌ ํธ๋ค๋ง: ์๋ชป๋ ์์ฒญ์ ์กฐ๊ธฐ์ ์ฐจ๋จํด ๋ถํ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
- ์ ์ง๋ณด์์ฑ: DTO๋ฅผ ๋ณด๋ ๊ฒ๋ง์ผ๋ก ์ด๋ค ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง๋์ง ๋ช ํํ๊ฒ ์ ์ ์์ต๋๋ค.
NestJS ํ๋ก์ ํธ์์ MongoDB๋ฅผ ์ฌ์ฉํ๋ค๋ฉด @IsMongoId()๋ ํ์ ๋๊ตฌ์
๋๋ค. ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ๋ถํฐ ์ปค์คํ
๊ฒ์ฆ๊ธฐ๊น์ง ๋ค์ํ ํ์ฉ๋ฒ์ ์ตํ๋๋ฉด ๋ ๊ฒฌ๊ณ ํ๊ณ ์์ ํ API๋ฅผ ๋ง๋ค ์ ์์ ๊ฑฐ์์.
์ง๊ธ ๋ฐ๋ก ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ class-validator๋ฅผ ์ค์นํ๊ณ @IsMongoId()๋ฅผ ์ ์ฉํด๋ณด์ธ์. ์์ ๋ณํ๊ฐ ํฐ ์ฐจ์ด๋ฅผ ๋ง๋ค์ด๋ผ ๊ฒ๋๋ค!