๐ก 04. DTO์ ๊ฒ์ฆ โ ์ฐ๋ ๊ธฐ ๋ฐ์ดํฐ๋ ์ ๊ตฌ ์ปท (Garbage In, Error Out)
๐ ๊ฐ์
์์ ํ ๋ฐ์ดํฐ ํต์ ์ ํต์ฌ์ธ DTO(Data Transfer Object)์ ํ์ดํ๋ฅผ ํ์ฉํ ๊ฐ๋ ฅํ ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ DTO์ class-validator โ ์ฒ ํต ๋ณด์ ๋ฌธ์ง๊ธฐ ๐ก
- ๐งฉ ๋ด์ฅ ํ์ดํ์ ์ ํธ๋ฆฌํฐ ๐ข
- ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ค์ ํ์๊ฐ์ DTO ๊น์๋ณด๊ธฐ
- ๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐๏ธ ์นํธ์ํธ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ
๐งณ ์ ์ ์ง์ ์ฒดํฌ๋ฆฌ์คํธ
- ๋ฐ์ฝ๋ ์ดํฐ(
@) ๋ฌธ๋ฒ์ ๊ธฐ๋ณธ ํํ๋ฅผ ์๋ค. - 02์ฅ ์ปจํธ๋กค๋ฌ์์ HTTP ๋ณธ๋ฌธ์ ๊ฐ์ ธ์ค๋
@Body()์ฌ์ฉ๋ฒ์ ๋ณด์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๋ฐ์ดํฐ ๊ฒ์ฆ์ ํ์์ฑ โ class-validator ์ค์ ๊ณผ ์ฌ์ฉ๋ฒ โ ํ์ดํ(Pipe)๋ก ํ๋ณํํ๊ธฐ โ ์ต์ ๋ฐฉ์์ธ Zod ์ฐ๋ โ ์์ฃผ ๊ฒช๋ ์๋ฌ
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ๋ฐ์ดํฐ๊ฐ ์ซ์์ธ์ง, ์ด๋ฉ์ผ ํ์์ธ์ง 100% ๋ณด์ฅํ๋ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
- ๋ถํ์ํ/์
์์ ์ธ ํ๋(
admin: true๋ฑ)๋ฅผ ์๋ฒ ์ ๊ตฌ์์ ์๋์ผ๋ก ๊ฑธ๋ฌ๋ผ ์ ์๋ค. - Zod ์คํค๋ง๋ฅผ ์ด์ฉํด DB ๋ชจ๋ธ๊ณผ ์๋ฒฝํ๊ฒ ์ผ์นํ๋ DTO๋ฅผ ๋ฝ์๋ผ ์ ์๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ
ํ๋ก ํธ์๋์์ ์๋ฌด๋ฆฌ ํผ(Form) ๊ฒ์ฆ์ ๋นก์ธ๊ฒ ํด๋๋, ํด์ปค๋ ์
์ฑ ๋ด์ Postman ๊ฐ์ ํด๋ก ์๋ฒ API์ ์ง์ ์ด์ํ ๋ฌธ์์ด์ ๊ฝ์ ๋ฃ์ด.
๋ง์ฝ ๋์ด(age) ํ๋์ {"age": "๋ฐฑ์ด"} ์ด๋ผ๋ ๋ฌธ์์ด์ด ๊ทธ๋๋ก DB์ ๋ค์ด๊ฐ๋ ค๊ณ ํ๋ค๋ฉด? ์๋ฒ๊ฐ ๋ป๊ฑฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ค์ผ๋๊ฒ ์ง?
DTO์ ํ์ดํ๋ ์ด๋ฐ "์น๋ช
์ ์ธ ์ฐ๋ ๊ธฐ ๋ฐ์ดํฐ"๋ฅผ ์ปจํธ๋กค๋ฌ์ ๋์ฐฉํ๊ธฐ๋ ์ ์ ๊ฐ์ฐจ ์์ด ์ณ๋ด๋ ๋ฐฉ์ด๋ง ์ด์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
DTO (Data Transfer Object) ๋ ๊น๊นํ ๊ณ ๊ธ ํด๋ฝ์ ์
์ฅ ์ ํ ๊ท์นํ ์ผ.
"๋์ด๋ ์ซ์์ฌ์ผ ํด. ์ด๋ฆ์ ๋ฌด์กฐ๊ฑด 2๊ธ์ ์ด์์ด์ด์ผ ํด. ์ด๋ฉ์ผ ํ์ ์๋๋ฉด ์
๊ตฌ ์ปท!"
ValidationPipe (์ ํจ์ฑ ๊ฒ์ฌ ํ์ดํ) ๋ ๊ทธ ๊ท์นํ๋ฅผ ๋ค๊ณ ์ ์๋ ๋ฉ์น ํฐ ๊ธฐ๋(๋ฌธ์ง๊ธฐ) ์ผ.
์๋(Request)์ด ์ค๋ฉด ์ ๋ถ์ฆ(Body ๋ฐ์ดํฐ)์ ์ซ ํ์ด๋ณด๊ณ , ๊ท์นํ์ ํ๋๋ผ๋ ์ด๊ธ๋๋ฉด "๋์๊ฐ๋ผ(400 Bad Request)" ํ๊ณ ๋ฌธ์ ๋ฐ๋ํด๋ฒ๋ ค. ๋๋ถ์ ํด๋ฝ ์(์ปจํธ๋กค๋ฌ์ ์๋น์ค)์ ํญ์ ๋ฌผ์ด ๊นจ๋ํ์ง!
๐งฉ DTO์ class-validator โ ์ฒ ํต ๋ณด์ ๋ฌธ์ง๊ธฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ ์ญ ํ์ดํ
useGlobalPipes์ ํ์ ์ค์ 3๊ฐ์ง๋ฅผ ์ค๋ช ํ ์ ์๋ค. @Is...๊ณ์ด ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์์ ๋กญ๊ฒ ์กฐํฉํ ์ ์๋ค.
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
TypeScript๋ ์ปดํ์ผ ํ์์๋ง ์๋ํด. ์คํ ์ค(๋ฐํ์)์ ํด๋ผ์ด์ธํธ๊ฐ ๋ชฐ๋ JSON์ ๋ณด๋ด๋ฉด TypeScript์interface๋ ๊ทธ๊ฑธ ๋ง์์ค ์ ์์๊น?
(์ ๋ต: ๋ชป ๋ง์! ๊ทธ๋์ ๋ฐํ์์ ์ด์๋จ๋class์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฐ๋ ๊ฑฐ์ผ.)
1๋จ๊ณ: ์ ์ญ ๊ธ๋ก๋ฒ ํ์ดํ(๊ธฐ๋) ์ธ์ฐ๊ธฐ
ํ๋ก์ ํธ ์ ์ฒด์ ๋จ ํ ๋ฒ๋ง ์ค์ ํ๋ฉด ๋ผ. (main.ts)
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 1. ๊ท์นํ์ ์๋ ์์ฑ์ ์ค์ฝ ๋ฌด์/์ญ์ ํด๋ฒ๋ฆผ (ํต์ฌ ๋ณด์!)
forbidNonWhitelisted: true, // 2. ๊ท์นํ์ ์๋ ๊ฑธ ๋ณด๋ด๋ฉด ์์ 400 ์๋ฌ๋ฅผ ๋ฑ์ (์ ํ)
transform: true, // 3. ๋ฌธ์์ด "1"์ ์์์ ์ซ์ 1๋ก ๋ฐ๊ฟ์ค (๊ฐ๊ฟ ๊ธฐ๋ฅ)
}));
await app.listen(3000);
}2๋จ๊ณ: DTO ํด๋์ค(๊ท์นํ) ๋ง๋ค๊ธฐ
ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํ๊ณ (npm i class-validator class-transformer),
๋ง์๋ง์ผ๋ก DTO ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋์ง๋์ง ๋ถ์ฌ์ฃผ๋ฉด ๋!
import { IsEmail, IsString, Length, IsOptional, Min } from 'class-validator';
export class CreateUserDto {
@IsEmail({}, { message: '์ ๋๋ก ๋ ์ด๋ฉ์ผ์ ์ ์ด์ฃผ์ธ์!' })
email: string;
@IsString()
@Length(2, 20)
name: string;
@IsOptional() // ๐ ์ ๋ณด๋ด๋ ์๊ด์๋ค๋ ๋ป!
@Min(14, { message: '14์ธ ์ด์๋ง ๊ฐ์
๊ฐ๋ฅํฉ๋๋ค.' })
age?: number;
}์ด์ ์ปจํธ๋กค๋ฌ์์ @Body() dto: CreateUserDto ๋ผ๊ณ ์ ๊ธฐ๋ง ํ๋ฉด, ์ปจํธ๋กค๋ฌ ๋ด๋ถ ์ฝ๋๋ถํฐ๋ dto.age๊ฐ 100% ์๋ฒฝํ ์ซ์ํ์ด๊ฑฐ๋, ์๋๋ฉด ์์ ์กด์ฌํ์ง ์๋๋ค๋ ๊ฒ ์ ๋ง ๋๊ณ ํ์ ํ ์ ์์ด.
๐งฉ ๋ด์ฅ ํ์ดํ์ ์ ํธ๋ฆฌํฐ ๐ข
๋ณต์กํ ๊ฐ์ฒด ๋ฉ์ด๋ฆฌ(@Body)๋ DTO์ class-validator๊ฐ ์ฒ๋ฆฌํด ์ฃผ์ง๋ง, ๋จ์ํ URL ๋ณ์(@Param, @Query) ๊ฒ์ฌ์๋ ๋ด์ฅ ํ์ดํ(Built-in Pipe) ๊ฐ ์ต๊ณ ์ผ.
๋ํ์ ์ธ ๋ด์ฅ ํ์ดํ 4๊ฐ์ง
ParseIntPipe: ๋ฌธ์์ด์ ๋ฌด์กฐ๊ฑด ์ ์๋ก ๋ฐ๊ฟ. ์คํจํ๋ฉด 400 ์๋ฌ!@Get(':id') findOne(@Param('id', ParseIntPipe) id: number) {}ParseBoolPipe: "true", "false", "1", "0" ๋ฑ์ ๊น๋ํ ๋ถ๋ฆฌ์ธ ํ์ ์ผ๋ก.ParseUUIDPipe: ๋ฌธ์์ด์ด UUID ํฌ๋งท์ธ์ง ๊น๊นํ๊ฒ ๊ฒ์ฆ.DefaultValuePipe: ์ฟผ๋ฆฌ์คํธ๋ง์ด ์ ๋์ด์์ ๋ ๊ธฐ๋ณธ๊ฐ ์ธํ .@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number
Mapped Types (์ฝ๋ ๋ณต๋ถ ๋ฐฉ์ง๊ธฐ)
ํ์๊ฐ์
์ฉ DTO(CreateDto)๊ฐ ์๋๋ฐ, ์ ๋ณด ์์ ์ฉ DTO(UpdateDto)๋ฅผ ๋ง๋ค๊ณ ์ถ์ ๋, ๋ด์ฉ๋ฌผ์ ๋ ๋ค ๋ณต๋ถํด์ผ ํ ๊น? NO!
import { PartialType } from '@nestjs/mapped-types';
// UpdateDto๋ CreateDto์ ๋๊ฐ์ง๋ง, ๋ชจ๋ ํ๋๊ฐ '์ ํ(Optional)'์ผ๋ก ๋ฐ๋!
export class UpdateUserDto extends PartialType(CreateUserDto) {}์ด ์ธ์๋ ํ๋ ์ผ๋ถ๋ง ๊ฐ์ ธ์ค๋ PickType, ์ผ๋ถ๋ง ๋ฒ๋ฆฌ๋ OmitType ๋ฑ ๊ฐ๋ ฅํ ์์ ๊ธฐ๋ฅ์ด ์ ๊ณต๋ผ.
๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ค์ ํ์๊ฐ์ DTO ๊น์๋ณด๊ธฐ
์์ฆ ๋์ธ์ธ Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด ๋ณผ ๊ฑฐ์ผ.
NestJS์ ์ ํต์ ์ธ class-validator ๋์ , Drizzle ์คํค๋ง์ 100% ํธํ๋๋ nestjs-zod๋ฅผ ์ฌ์ฉํ๋ ํ๋์ ์ธ ๋ฐฉ์์ด์ง!
import { createZodDto } from 'nestjs-zod';
import { insertUserSchema } from '@repo/schema'; // Drizzle์์ ์๋ ์์ฑํ Zod ์คํค๋ง
// 1. Zod ์คํค๋ง์์ ํ์ํ ์์ฑ๋ง ์ ๋นผ์จ๋ค (.pick)
const RegisterSchema = insertUserSchema.pick({
email: true,
password: true,
name: true,
});
// 2. ์์์ ์๋ฒฝํ DTO ํด๋์ค๋ก ๋๊ฐ!
export class RegisterDto extends createZodDto(RegisterSchema) {}
// ----- ์ปจํธ๋กค๋ฌ -----
@Post('register')
async register(@Body() dto: RegisterDto) {
// dto.email, dto.password, dto.name ์๋ ์์ฑ ์ง์!
// ๋ฌด์กฐ๊ฑด ๊ฒ์ฆ๋ ์์ ํ ๋ฐ์ดํฐ!
return this.authService.register(dto);
}โ
Drizzle + Zod์ ์๋์ ์ธ ์ฅ์ :
DB ํ
์ด๋ธ ๊ตฌ์กฐ(Schema)๋ฅผ ๋ฐ๊พธ๋ฉด? Zod ์คํค๋ง๋ ์์์ ๋ฐ๋๊ณ , DTO ํด๋์ค๋ ์์์ ๋ฐ๋์ด! ํด๋ผ์ด์ธํธ๋ถํฐ DB๊น์ง ๋จ์ผ ์ง์ค ๊ณต๊ธ์(SSOT) ์ด ์์ฑ๋๋ ์ง๋ฆฟํ ๊ฒฝํ์ ํ ์ ์์ด.
๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ ๐ก
1. ์ ์ญ whitelist: true๋ ๋ชฉ์จ๊ณผ๋ ๊ฐ๋ค.
ํด๋ผ์ด์ธํธ ๋ง๋๋ก { "email": "a@a.com", "role": "admin" } ์ด๋ ๊ฒ role ํ๋๋ฅผ ๊ฐ์ ๋ก ์ค์
๋ฃ์ผ๋ฉด ๊ถํ ํ์ทจ ๋ฒ๊ทธ๊ฐ ํฐ์ง ์ ์์ด.
whitelist: true๋ฅผ ์ผ๋๋ฉด DTO์ ์ ํ ์์ง ์์ ํ๋(role)๋ ๋ฌธ์ง๊ธฐ๊ฐ ๋ฌด์ฐธํ ์ฐ์ด๋ฒ๋ฆฌ๊ณ ์์ ํ ๋ฐ์ดํฐ๋ง ํต๊ณผ์์ผ์ค.
2. ์ค์ฒฉ๋ ๊ฐ์ฒด ๋ฐฐ์ด ๊ฒ์ฆ
์ฌ์ฉ์๊ฐ "์ฅ๋ฐ๊ตฌ๋ ๋ฐฐ์ด"์ ์ ์กํ๋ค๋ฉด, ๊ฐ ๋ฐฐ์ด ์์ดํ
๋ ์ผ์ผ์ด ๊ฒ์ฆํด์ผ ํด.
์ด๋๋ @ValidateNested()๊ฐ ํ์์ผ.
import { ValidateNested, Type } from 'class-transformer';
class OrderItemDto { ... }
class CreateOrderDto {
// 1. ์ด๊ฑฐ ๋ฐฐ์ด์ธ์ง ๊ฒ์ฌ
@IsArray()
// 2. ๋ด๋ถ๋ฅผ ์
์
์ด ๋ค์ ธ์ค๊ฒ!
@ValidateNested({ each: true })
// 3. ์๋ฐ์คํฌ๋ฆฝํธ๋ ๋ฐํ์์ ๋ฐฐ์ด ์์ ํ์
์ ๋ชจ๋ฅด๋๊น Type์ ๋ฌ์์ค์ผ ํด!
@Type(() => OrderItemDto)
items: OrderItemDto[];
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ Validation failed (numeric string is expected)
ํ์:
{ "statusCode": 400, "message": "Validation failed (numeric string is expected)", "error": "Bad Request" }์์ธ: ์ปจํธ๋กค๋ฌ ํ๋ผ๋ฏธํฐ์์ ParseIntPipe๋ฅผ ๋ฌ์๋จ๋๋ฐ ํด๋ผ์ด์ธํธ๊ฐ ์ซ์๊ฐ ์๋ ๊ธ์(์: ?page=abc)๋ฅผ ๋ณด๋๊ธฐ ๋๋ฌธ.
ํด๊ฒฐ์ฑ
:
๋ด ์ฝ๋๊ฐ ์๋ชป๋ ๊ฒ ์๋์ผ. ๋ฐฉ์ด์ ์ฑ๊ณตํ ๊ฑฐ๋๊น ํ๋ก ํธ๊ฐ๋ฐ์์๊ฒ "์ซ์๋ก ์ฃผ์ธ์~" ๋ผ๊ณ ๋งํ๋ฉด ๋ผ.
โ property 'nestedObject' should not exist (whitelist ์๋ฌ)
์์ธ:
์ ์ญ ์ค์ ์ whitelist: true, forbidNonWhitelisted: true๊ฐ ์ผ์ ธ ์๋๋ฐ, DTO ๊ท์น์ ๋ช
์๋์ง ์์ ์ด์ํ ์์ฑ์ ํด๋ผ์ด์ธํธ๊ฐ ํ์ด๋ก๋์ ๊ปด์ ๋ณด๋.
ํด๊ฒฐ์ฑ
:
ํ์ํ ๋ฐ์ดํฐ๊ฐ ๋ง๋ค๋ฉด DTO ํด๋์ค์ ์์ฑ์ ์ถ๊ฐํด. (๋ง์ฝ ๊ตณ์ด ๊ฒ์ฌํ ํ์ ์๋ ์์ ํ์ ๋ฐ์ดํฐ๋ผ๋ฉด @IsOptional() ์ด๋ผ๋ ๋ฌ์์ค์ผ ํด)
๐๏ธ ์นํธ์ํธ โ ์ค๋ฌด ์์ฝ ์นด๋
| ์ํฉ | ๋ฐ์ฝ๋ ์ดํฐ / ์ค์ | ์ค๋ช |
|---|---|---|
| ๋ฌธ์์ด์ ๊ธธ์ด๋ฅผ ์ ํํ๊ณ ์ถ์ด์ | @Length(2, 10) | ์ต์ 2, ์ต๋ 10์ |
| ํน์ ๋ฌธ์์ด ์ ์ค 1๊ฐ์ฌ์ผ ํด์ | @IsIn(['USER', 'ADMIN']) | Enum ๋๋์ ๊ฒ์ฆ |
| ๋ฐฐ์ด์ด ๋น์ด์์ผ๋ฉด ์ ๋ผ์ | @ArrayMinSize(1) | length๊ฐ ์ต์ 1 ์ด์ |
| ์ ํ์ ๊ฐ์ด๋ผ ์ ๋ค์ด์๋ ๋ผ์ | @IsOptional() | null, undefined ํจ์ค |
| ๋ณต์กํ ๊ฐ์ฒด/๋ฐฐ์ด ๋ด๋ถ๋ฅผ ๊ฒ์ฌํ ๋์ | @Type(() => Class) @ValidateNested() | ์ด๊ฒ๋ค์ ์ธํธ๋ก ๋ค๋ ์ผ ์๋ํจ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. class-validator๋ฅผ ์ด์ฉํ ๋ ์๋ ์ค์ ์ค, ํด์ปค๊ฐ ์
์์ ์ธ ์์ฌ ํ๋(isPaid: true ๋ฑ)๋ฅผ JSON์ ์์ด ๋ณด๋ด๋ ๊ฒ์ ๋ง๊ธฐ ์ํด ๋ฐ๋์ ์ผ์ผ ํ๋ ์ต์
์?
app.useGlobalPipes(new ValidationPipe({
__________: true
}));- A)
transform - B)
whitelist - C)
disableErrorMessages - D)
enableCors
โ ์ ๋ต: B
๐ก ์ค๋ช
: whitelist: true๋ ๋ง์น ํ์ดํธ๋ฆฌ์คํธ(ํ๊ฐ ๋ช
๋จ) ๊ธฐ๋ฐ ์ถ์
ํต์ ์ ๊ฐ์. DTO์ ์ด๋ฆ์ด ์ ์ ํ์๋ ๋ฐ์ดํฐ ํ๋๋ ์กฐ์ฉํ ํ๊ธฐ์ฒ๋ถ ํด๋ฒ๋ ค!
Q2. ๋ฌธ์์ด ํํ์ ์ฟผ๋ฆฌ์คํธ๋ง ?limit=50 ์ ์ซ์์ธ 50์ผ๋ก ์์ ํ๊ฒ ํ ๋ณํํ๋ฉด์ ๋ค์ผ๋ก ๋ฌธ์์ธ์ง ์๋์ง ์ ํจ์ฑ ๊ฒ์ฌ๊น์ง ๊ณต์ง๋ก ํด์ฃผ๋ ๋ง๋ฒ ๊ฐ์ ๋ด์ฅ ํ์ดํ๋ ๋ฌด์์ธ๊ฐ?
โ
์ ๋ต: ParseIntPipe
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ์ ์(Int)๋ก ๋ถ์(Parse) ํ์ดํ!
Q3. ๋๋ฒ๊น ํด์ฆ: ์๋ ์ฝ๋์์ ๊ฐ์ฒด ๋ฐฐ์ด ๊ฒ์ฆ์ด ์ ๋๋ก ๋์ํ์ง ์๋ ์์ธ์ ์ฐพ์๋ด.
import { IsArray, ValidateNested } from 'class-validator';
class TaskDto { @IsString() title: string; }
class CreateProjectDto {
@IsArray()
@ValidateNested({ each: true })
tasks: TaskDto[];
}ํ
์คํธ๋ก tasks: [{ title: 12345 }] ๋ผ๊ณ ์ซ์๋ฅผ ๋ฃ์ด์ ๋ณด๋๋๋ฐ, ์๋ฌ๊ฐ ์ ๋๊ณ ํต๊ณผํด๋ฒ๋ฆฐ๋ค?!
โ
์ ๋ต: @Type(() => TaskDto) ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋นผ๋จน์๊ธฐ ๋๋ฌธ!
**์ค๋ช
:**TypeScript์ ํ์
์ ๋ณด๋ ์ปดํ์ผ ํ JS๋ก ๋ณํ๋ฉด ๊ฐ์ชฝ๊ฐ์ด ์ฌ๋ผ์ ธ. ๋ฐํ์์ ๋๋ ํ์ดํ ์
์ฅ์์ ์ tasks ๋ฐฐ์ด ์์ ์์๊ฐ TaskDto ๊ฐ์ฒด๊ฐ ๋ง๋์ง ์ ๊ธธ์ด ์์ด. ๋ฌด์กฐ๊ฑด ๋ช
์์ ์ผ๋ก @Type ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ฌ์์ค์ผ ์ค์ฒฉ ๊ฒ์ฆ์ด ์๋ํด!