Next.js์์ class-validator๋ก ํ์ ์์ ์ฑ ๋์ด๊ธฐ
Next.js๋ก ๊ฐ๋ฐํ ๋ API ๋ผ์ฐํธ๋ ํผ์์ ๋์ด์ค๋ ๋ฐ์ดํฐ์ ์ ํจ์ฑ์ ์ด๋ป๊ฒ ๊ฒ์ฆํ๊ณ ๊ณ์ ๊ฐ์? Express๋ NestJS์ ๊ฐ์ ํ๋ ์์ํฌ์ ๋ฌ๋ฆฌ, Next.js์๋ ๋ด์ฅ๋ ์ ํจ์ฑ ๊ฒ์ฆ ๊ณ์ธต์ด ์์ต๋๋ค. ์ด ๋๋ฌธ์ ๊ฐ๋ฐ์๊ฐ ์ง์ ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ ํ์ธํ๋ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ์ฃ .
์ด ๊ธ์์๋ NestJS ๊ฐ๋ฐ์๋ค์๊ฒ ์ต์ํ class-validator ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ Next.js ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํฉ๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ์ ๊น๋ํ ์ ํจ์ฑ ๊ฒ์ฆ ๋ก์ง์ ํตํด ์ด๋ป๊ฒ ๋ ์์ ์ ์ด๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ข์ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ค์ ๋ก ๊ฐ์ธ์ ์ธ ๊ฒฝํ ์ class-validator ๋์
ํ ์ ํ ์์ ์ฑ๊ณผ ๊ฐ๋ฐ ํธ์์ฑ์ด ํจ์ฌ ์ฆ๊ฐํ์ต๋๋ค. Nextjs๋ API ์๋ฒ๋ฅผ ๋ง๋๋ ๋ฐ์ ๋ถํธํฉ๋๋ค. ๊ทธ๋ ์ง๋ง ์ํคํ
์ณ๋ฅผ ์ ์ค๊ณํ๋ฉด Next.js์์๋ ๊ฝค ์ธ๋งํ API ์๋ฒ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
ํด๋น ์ํฐํด์ Next.js 15๋ฒ์ ์ ๊ธฐ์ค์ผ๋ก ์์ฑ๋์์ต๋๋ค. ์ฐธ๊ณ ํด์ฃผ์ธ์. ๐
์ Next.js์์ class-validator๋ฅผ ์ฌ์ฉํด์ผ ํ ๊น์?
Next.js์์ API ์์ฒญ ๋ณธ๋ฌธ(body)์ ์ฒ๋ฆฌํ ๋, ๊ฐ ํ๋๊ฐ ์ฌ๋ฐ๋ฅธ ํ์ ์ธ์ง, ํ์๊ฐ์ด ๋๋ฝ๋์ง๋ ์์๋์ง ์ผ์ผ์ด ํ์ธํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ ๋ฒ๊ฑฐ๋ก์ด ์ผ์ ๋๋ค.
// ์ผ๋ฐ์ ์ธ ์ ํจ์ฑ ๊ฒ์ฆ ๋ก์ฝ์ง
if (!body.email || typeof body.email !== 'string') {
// ์๋ฌ ์ฒ๋ฆฌ...
}
if (!body.password || body.password.length < 6) {
// ์๋ฌ ์ฒ๋ฆฌ...
}
์ด๋ฐ ๋ฐฉ์์ ์ฝ๋๋ฅผ ์ง์ ๋ถํ๊ฒ ๋ง๋ค๊ณ , ์๋ก์ด ํ๋๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค ๊ฒ์ฆ ๋ก์ง์ ์์ ํด์ผ ํ๋ ๋ถํธํจ์ด ๋ฐ๋ฆ ๋๋ค.
class-validator๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค. DTO(Data Transfer Object) ํด๋์ค์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ด์ฉํด ์ ํจ์ฑ ๊ฒ์ฆ ๊ท์น์ ์ ์ธ์ ์ผ๋ก ์ ์ํ ์ ์์ด, ์ฝ๋๊ฐ ํจ์ฌ ๊ฐ๊ฒฐํ๊ณ ์ง๊ด์ ์ผ๋ก ๋ณํฉ๋๋ค.
class-validator ์ค์ ํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Next.js ํ๋ก์ ํธ์ class-validator๋ฅผ ์ค์ ํด ๋ด
์๋ค.
1. ํ์ ํจํค์ง ์ค์นํ๊ธฐ
๋จผ์ , ์ ํจ์ฑ ๊ฒ์ฆ์ ํ์ํ ํจํค์ง๋ค์ ์ค์นํด์ผ ํฉ๋๋ค.
npm install class-validator class-transformer reflect-metadata
class-validator: ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.class-transformer: ์์(plain) ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ์ฒด๋ฅผ ํด๋์ค ์ธ์คํด์ค๋ก ๋ณํํด์ค๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ๋๋ก ์ฝ์ผ๋ ค๋ฉด ์ด ๋ณํ ๊ณผ์ ์ด ํ์์ ์ ๋๋ค.reflect-metadata: ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ฐํ์์ ํ์ ์ ๋ณด๋ฅผ ์ฝ์ ์ ์๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
๋ฐ์ฝ๋ ์ดํฐ ๋ฌธ๋ฒ์ ์ ๋๋ก ์ฌ์ฉํ๊ธฐ ์ํด Babel ๊ด๋ จ ํจํค์ง๋ ์ค์นํด์ผ ํฉ๋๋ค.
npm install --save-dev @babel/core babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators babel-plugin-parameter-decorator
2. tsconfig.json ์ค์ ํ๊ธฐ
TypeScript๊ฐ ๋ฐ์ฝ๋ ์ดํฐ ๋ฌธ๋ฒ์ ์ฌ๋ฐ๋ฅด๊ฒ ํด์ํ๊ณ , ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์์ฑํ ์ ์๋๋ก tsconfig.json ํ์ผ์ ์์ ํด์ผ ํฉ๋๋ค. compilerOptions์ ๋ค์ ๋ ๊ฐ์ง ์์ฑ์ ์ถ๊ฐํด์ฃผ์ธ์.
// tsconfig.json
{
"compilerOptions": {
// ... ๊ธฐ์กด ์ค์
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
experimentalDecorators: TypeScript์์ ๋ฐ์ฝ๋ ์ดํฐ ์ฌ์ฉ์ ํ์ฉํฉ๋๋ค.emitDecoratorMetadata:reflect-metadata๊ฐ ๋ฐํ์์ ํ์ ์ ๋ณด๋ฅผ ์ฐธ์กฐํ ์ ์๋๋ก ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์์ฑํฉ๋๋ค.
3. .babelrc ์ค์ ํ๊ธฐ
ํ๋ก์ ํธ ๋ฃจํธ์ .babelrc ํ์ผ์ ์์ฑํ๊ณ , ๋ฐ์ฝ๋ ์ดํฐ ๊ด๋ จ ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํฉ๋๋ค. ์ด ์ค์ ์ Babel์ด TypeScript ์ฝ๋๋ฅผ ๋ณํํ ๋ ๋ฐ์ฝ๋ ์ดํฐ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ์งํ๋๋ก ๋์ต๋๋ค.
// .babelrc
{
"presets": ["next/babel"],
"plugins": [
"babel-plugin-transform-typescript-metadata",
["@babel/plugin-proposal-decorators", { "legacy": true }],
"babel-plugin-parameter-decorator",
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}
4. reflect-metadata ์ ์ญ์ผ๋ก ๋ถ๋ฌ์ค๊ธฐ
reflect-metadata๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์์์ ์์ ๋จ ํ ๋ฒ๋ง ๋ถ๋ฌ์์ผ ํฉ๋๋ค. API ๋ผ์ฐํธ์ ์ง์
์ ์ด๋ ๊ณตํต ์ ํธ๋ฆฌํฐ ํ์ผ ์๋จ์ import ๊ตฌ๋ฌธ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์๋ฅผ ๋ค์ด src/lib/validation.ts์ ๊ฐ์ ํ์ผ์ ๋ง๋ค์ด ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
// src/lib/validation.ts ๋๋ app/api/.../route.ts ์๋จ
import "reflect-metadata";
// ... ์ดํ ์ฝ๋
DTO(Data Transfer Object) ์ ์ํ๊ธฐ
์ด์ ์ ํจ์ฑ ๊ฒ์ฆ ๊ท์น์ ๋ด์ DTO ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค. src/dto/create-user.dto.ts ํ์ผ์ ์์ฑํ๊ณ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํฉ๋๋ค.
// src/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: '์ด๋ฆ์ ํ์ ํญ๋ชฉ์
๋๋ค.' })
name: string;
@IsEmail({}, { message: '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.' })
email: string;
@MinLength(6, { message: '๋น๋ฐ๋ฒํธ๋ ์ต์ 6์ ์ด์์ด์ด์ผ ํฉ๋๋ค.' })
password: string;
@IsNotEmpty({ message: 'ํ๊ต ์ ๋ณด๋ ํ์ ํญ๋ชฉ์
๋๋ค.' })
school: string;
@IsNotEmpty({ message: '์ ํ๋ฒํธ๋ ํ์ ํญ๋ชฉ์
๋๋ค.' })
phoneNumber: string;
@IsOptional()
introduce?: string;
}
๊ฐ ์์ฑ ์์ @IsEmail(), @MinLength(6)์ ๊ฐ์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ถ์ฌ ๊ฐ๋จํ๊ฒ ์ ํจ์ฑ ๊ฒ์ฆ ๊ท์น์ ์ ์ํ ์ ์์ต๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ์ ์ปค์คํ
์๋ฌ ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ฉด, ๊ฒ์ฆ ์คํจ ์ ๋ ์น์ ํ ํผ๋๋ฐฑ์ ์ ๊ณตํ ์ ์์ด์.
์ ํจ์ฑ ๊ฒ์ฆ ์ ํธ๋ฆฌํฐ ๋ง๋ค๊ธฐ
NestJS์ ValidationPipe์ ๊ฐ์ ๊ธฐ๋ฅ์ด Next.js์๋ ์๊ธฐ ๋๋ฌธ์, DTO๋ฅผ ๊ฒ์ฆํ๋ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ์ง์ ๋ง๋ค์ด์ผ ํฉ๋๋ค. src/lib/validation.ts ํ์ผ์ ๋ค์ ํจ์๋ฅผ ์ถ๊ฐํด๋ด
์๋ค.
// src/lib/validation.ts
import 'reflect-metadata';
import { validate, ValidationError } from 'class-validator';
import { plainToInstance } from 'class-transformer';
export async function validateDto<T extends object>(
dtoClass: new () => T,
payload: unknown,
): Promise<{ instance: T; errors: string[] }> {
const instance = plainToInstance(dtoClass, payload);
const errors: ValidationError[] = await validate(instance);
if (errors.length > 0) {
const errorMessages = errors
.map((err) => Object.values(err.constraints || {}))
.flat();
return { instance, errors: errorMessages };
}
return { instance, errors: [] };
}
์ด ํจ์๋ ๋ ๊ฐ์ง ์ค์ํ ์ญํ ์ ํฉ๋๋ค.
plainToInstance(dtoClass, payload): ์ผ๋ฐ JavaScript ๊ฐ์ฒด(payload)๋ฅผ ์ฐ๋ฆฌ๊ฐ ์ ์ํ DTO ํด๋์ค(dtoClass)์ ์ธ์คํด์ค๋ก ๋ณํํฉ๋๋ค. ์ด ๊ณผ์ ์ ๊ฑฐ์ณ์ผ ๋ฐ์ฝ๋ ์ดํฐ์ ์ ์๋ ๋ฉํ๋ฐ์ดํฐ๊ฐ ํ์ฑํ๋ฉ๋๋ค.validate(instance): ๋ณํ๋ ์ธ์คํด์ค์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๊ณ , ์ค๋ฅ๊ฐ ์๋ค๋ฉดValidationError๊ฐ์ฒด์ ๋ฐฐ์ด์ ๋ฐํํฉ๋๋ค.
API Route์์ ์ฌ์ฉํ๊ธฐ
์ด์ ๋ง๋ DTO์ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ API ๋ผ์ฐํธ์์ ์ฌ์ฉํด๋ณผ ์ฐจ๋ก์
๋๋ค. app/api/users/route.ts ํ์ผ์ ์์๋ก ๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { CreateUserDto } from '@/dto/create-user.dto';
import { validateDto } from '@/lib/validation';
export async function POST(req: Request) {
try {
const body = await req.json();
const { instance, errors } = await validateDto(CreateUserDto, body);
if (errors.length > 0) {
// ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ
return NextResponse.json(
{ message: 'Validation failed', errors },
{ status: 400 },
);
}
// ์ ํจ์ฑ ๊ฒ์ฆ ํต๊ณผ โ DB ์ ์ฅ ๋๋ ๋น์ฆ๋์ค ๋ก์ง ํธ์ถ
// ...
return NextResponse.json({
message: 'โ
Validation passed',
data: instance,
});
} catch (err: any) {
// req.json() ํ์ฑ ์๋ฌ ๋ฑ ๊ธฐํ ์์ธ ์ฒ๋ฆฌ
return NextResponse.json(
{ message: 'An unexpected error occurred' },
{ status: 500 },
);
}
}
POST ์์ฒญ์ด ๋ค์ด์ค๋ฉด req.json()์ผ๋ก ๋ฐ์ ์์ฒญ ๋ณธ๋ฌธ์ validateDto ํจ์์ ์ ๋ฌํฉ๋๋ค. ๋ง์ฝ errors ๋ฐฐ์ด์ ๋ด์ฉ์ด ์๋ค๋ฉด, 400 ์ํ ์ฝ๋์ ํจ๊ป ์๋ฌ ๋ฉ์์ง๋ฅผ ์๋ตํฉ๋๋ค. ๊ฒ์ฆ์ ํต๊ณผํ๋ค๋ฉด, instance ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ๋ค์ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ฉด ๋ฉ๋๋ค. instance๋ CreateUserDto ํด๋์ค์ ์ธ์คํด์ค์ด๋ฏ๋ก ํ์
์ด ๋ณด์ฅ๋ฉ๋๋ค.
Controller ์ถ๊ฐํ๊ธฐ
๋ผ์ฐํธ์ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ถ๋ฆฌํด์ ์ปจํธ๋กค๋ฌ์์ ๊ฒ์ฆ์ ํด๋ณผ ์๋ ์์ต๋๋ค:
class UserController {
async createUser(payload: CreateUserDto) {
await guardAdmin();
await validateDto(CreateUserDto, payload);
return userService.createUserByAdmin(payload);
}
}
์ด๋ ๊ฒ ํ๋ฉด ๊ด์ฌ์ฌ๋ฅผ ๋์ฑ ๋ช
ํํ๊ฒ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก Service์ Repository๋ ๋ง๋ค์ด๋ณผ ์ ์๊ฒ ์ฃ ?
๋ ๋์ ๊ฐ๋ฐ ๊ฒฝํ์ ์ํ์ฌ
Next.js์์ class-validator๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋จ์ํ ์ ํจ์ฑ ๊ฒ์ฆ ์ฝ๋๋ฅผ ์ค์ด๋ ๊ฒ ์ด์์ ์๋ฏธ๋ฅผ ๊ฐ์ง๋๋ค.
- ๊ฐ๋ ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ ํฅ์: ์ ํจ์ฑ ๊ฒ์ฆ ๊ท์น์ด DTO์ ๋ช ํํ๊ฒ ์ ์ธ๋์ด ์์ด, ์ฝ๋๋ฅผ ์ดํดํ๊ณ ์์ ํ๊ธฐ ์ฌ์์ง๋๋ค.
- ํ์
์์ ์ฑ ํ๋ณด:
plainToInstance๋ฅผ ํตํด ๋ณํ๋ ๊ฐ์ฒด๋ ํด๋์ค ํ์ ์ด ๋ณด์ฅ๋๋ฏ๋ก, ๊ฐ๋ฐ ๊ณผ์ ์์ ํ์ ๊ด๋ จ ์ค์๋ฅผ ์ค์ผ ์ ์์ต๋๋ค. - ์ ์ธ์ ํ๋ก๊ทธ๋๋ฐ: โ์ด๋ป๊ฒโ ๊ฒ์ฆํ ์ง๊ฐ ์๋, โ๋ฌด์์โ ๊ฒ์ฆํ ์ง์ ์ง์คํ๊ฒ ๋์ด ๋ ๊น๋ํ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
์กฐ๊ธ์ ์ด๊ธฐ ์ค์ ์ด ํ์ํ์ง๋ง, class-validator๊ฐ ์ ๊ณตํ๋ ์์ ์ฑ๊ณผ ๊ฐ๋ฐ ํธ์์ฑ์ ๊ณ ๋ คํ๋ฉด ์ถฉ๋ถํ ํฌ์ํ ๊ฐ์น๊ฐ ์์ต๋๋ค. ์ฌ๋ฌ๋ถ์ Next.js ํ๋ก์ ํธ์๋ ๋์
ํ์ฌ ๋ ๊ฒฌ๊ณ ํ๊ณ ํ์
-์์ ํ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค์ด๋ณด์ธ์.
References
- How to Use class-validator in a Next.js App
- ๊ทธ๋ฆฌ๊ณ ๋ด ์ฝ๋ ๐