반응형
Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- react
- 회고
- 알고리즘
- array
- WIL
- 기록
- nest.js
- 피드백
- mongoose
- Grafana
- mysql
- 코테
- 리눅스
- 주간회고
- typescript
- js
- javascript
- 네트워크
- next.js
- 생각로그
- 생각일기
- 자바스크립트
- Git
- CS
- Java
- 트러블슈팅
- mongo
- til
- MongoDB
- 생각정리
Archives
- Today
- Total
코딩일상
[nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Pipes] 본문
반응형

1. Pipes의 정의와 역할
Pipes는 @Injectable() 데코레이터가 붙은 클래스로, PipeTransform 인터페이스를 구현합니다. 컨트롤러 라우트 핸들러가 실행되기 전에 메서드의 인자를 가로채서 처리하는 역할을 합니다.
두 가지 주요 사용 사례:
- Transformation(변환): 입력 데이터를 원하는 형태로 변환 (예: string → integer)
- Validation(검증): 입력 데이터 검증 후 통과 또는 예외 발생
중요한 특징:
- Pipes는 예외 처리 영역(exceptions zone) 내에서 실행됩니다
- Pipe에서 예외가 발생하면 전역 예외 필터나 해당 컨텍스트의 예외 필터가 처리합니다
- Pipe에서 예외가 발생하면 컨트롤러 메서드는 실행되지 않습니다
- 외부 소스에서 들어오는 데이터를 시스템 경계에서 검증하는 베스트 프랙티스를 제공합니다
2. Built-in Pipes (내장 파이프)
NestJS는 다음과 같은 내장 Pipes를 제공합니다 (@nestjs/common 패키지):
// 변환(Transformation) Pipes
ParseIntPipe // string → integer
ParseFloatPipe // string → float
ParseBoolPipe // string → boolean
ParseArrayPipe // string → array
ParseUUIDPipe // string 검증 및 UUID로 변환
ParseEnumPipe // enum 값 검증 및 변환
ParseDatePipe // string → Date
DefaultValuePipe // 기본값 제공
ParseFilePipe // 파일 검증
// 검증(Validation) Pipe
ValidationPipe // 객체 검증
3. Pipe Binding (파이프 바인딩)
Pipes는 여러 레벨에서 바인딩할 수 있습니다:
3.1 Parameter-scoped (파라미터 레벨)
// 기본 사용 - 클래스 전달 (DI 활용)
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// 인스턴스 전달 - 옵션 커스터마이징
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE
}))
id: number,
) {
return this.catsService.findOne(id);
}
// Query 파라미터에 적용
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// UUID 검증
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
3.2 Method-scoped (메서드 레벨)
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
3.3 Global-scoped (전역 레벨)
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
// 또는 모듈에서 DI 활용
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
4. Custom Pipes 만들기
4.1 기본 구조
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
PipeTransform 인터페이스:
interface PipeTransform<T = any, R = any> {
transform(value: T, metadata: ArgumentMetadata): R;
}
ArgumentMetadata 구조:
interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>; // String, Number 등의 타입
data?: string; // 데코레이터에 전달된 문자열
}
4.2 Transformation Pipe 예제
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
5. Schema-based Validation
5.1 Zod를 사용한 검증
// 설치
// npm install --save zod
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
// Schema 정의
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
// 사용
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
주의: Zod는 tsconfig.json에서 strictNullChecks 설정이 필요합니다.
5.2 class-validator를 사용한 검증
// 설치
// npm i --save class-validator class-transformer
// DTO 정의
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
// ValidationPipe 구현
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 검증이 필요 없는 타입 체크
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// plain object → typed object 변환
const object = plainToInstance(metatype, value);
// 검증 실행
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
// 사용
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
class-validator를 사용하는 이유:
- DTO 클래스가 데이터 구조와 검증 로직의 단일 소스(Single Source of Truth)가 됩니다
- 데코레이터 기반으로 깔끔하고 선언적입니다
- metatype 정보를 활용하여 런타임에 타입 검증이 가능합니다
plainToInstance가 필요한 이유: 네트워크 요청에서 역직렬화된 body 객체는 타입 정보가 없는 plain JavaScript 객체입니다. class-validator가 DTO에 정의된 데코레이터를 사용하려면 적절한 타입 정보를 가진 객체로 변환해야 합니다.
이 코드의 흐름을 단계별로 완벽 설명
1. 요청이 들어왔을 때의 데이터 상태
// 클라이언트가 POST /cats 요청
// Body: { "name": "Kitty", "age": 3, "breed": "Persian" }
// Express/Fastify가 받은 데이터 (JSON.parse 후)
const incomingData = {
name: "Kitty",
age: 3,
breed: "Persian"
};
// 이 시점의 문제점:
console.log(incomingData instanceof CreateCatDto); // false
console.log(typeof incomingData); // "object"
// 그냥 plain JavaScript 객체일 뿐, CreateCatDto 클래스의 인스턴스가 아님!
2. ValidationPipe의 transform 메서드 실행 순서
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 📍 STEP 1: 입력값 분석
console.log('value:', value);
// { name: "Kitty", age: 3, breed: "Persian" }
console.log('metatype:', metatype);
// [Function: CreateCatDto] - 실제 CreateCatDto 클래스 자체
// 📍 STEP 2: 검증이 필요한지 체크
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// 📍 STEP 3: 변환 (핵심!)
const object = plainToInstance(metatype, value);
// 📍 STEP 4: 검증
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
// 📍 STEP 5: 원본 또는 변환된 값 반환
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
3. 각 단계 상세 설명
STEP 1: ArgumentMetadata 이해
// @Body() 데코레이터가 제공하는 메타데이터
const metadata: ArgumentMetadata = {
type: 'body', // 어디서 온 데이터인가? (body, query, param, custom)
metatype: CreateCatDto, // 어떤 타입이어야 하는가?
data: undefined // 데코레이터에 전달된 키 (예: @Body('user')면 'user')
};
// 왜 metatype이 중요한가?
// TypeScript 컴파일 후에도 런타임에서 CreateCatDto 클래스 정보를 얻을 수 있음!
STEP 2: toValidate() 메서드 - 왜 필요한가?
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
// 사용 예시:
toValidate(String); // false - 검증 불필요 (기본 타입)
toValidate(Number); // false - 검증 불필요
toValidate(CreateCatDto); // true - 검증 필요! (커스텀 클래스)
// 이유:
// @Query('page') page: number 같은 경우
// - metatype은 Number (기본 JavaScript 타입)
// - 기본 타입에는 class-validator 데코레이터를 붙일 수 없음
// - 검증할 필요가 없음 (이미 타입 변환만 필요)
STEP 3: plainToInstance() - 가장 중요한 부분!
// 변환 전
const plainObject = { name: "Kitty", age: 3, breed: "Persian" };
console.log(plainObject instanceof CreateCatDto); // false
console.log(plainObject.constructor.name); // "Object"
// plainToInstance 실행
const object = plainToInstance(CreateCatDto, plainObject);
// 변환 후
console.log(object instanceof CreateCatDto); // true
console.log(object.constructor.name); // "CreateCatDto"
// 내부적으로 일어나는 일:
// 1. new CreateCatDto() 인스턴스 생성
// 2. plainObject의 속성들을 새 인스턴스에 복사
// 3. 클래스 메타데이터 보존 (데코레이터 정보 포함!)
왜 이 변환이 필요한가?
export class CreateCatDto {
@IsString() // 👈 이 메타데이터는 CreateCatDto 클래스에 저장됨
name: string;
@IsInt() // 👈 이것도 CreateCatDto 클래스에 저장됨
age: number;
@IsString()
breed: string;
}
// plain object는 이 데코레이터 정보를 가지고 있지 않음!
const plain = { name: "Kitty", age: 3, breed: "Persian" };
// plain 객체에는 @IsString(), @IsInt() 같은 메타데이터가 없음
// class-validator의 validate() 함수는
// 클래스 인스턴스의 메타데이터를 읽어서 검증을 수행함
// 따라서 반드시 CreateCatDto 인스턴스여야 함!
STEP 4: validate() 실행
import { validate } from 'class-validator';
const object = plainToInstance(CreateCatDto, value);
const errors = await validate(object);
// validate()가 하는 일:
// 1. object의 클래스(CreateCatDto)에서 메타데이터 읽기
// 2. 각 속성에 붙은 데코레이터 확인
// 3. 실제 값이 규칙을 만족하는지 검사
// 예시:
const invalidData = { name: 123, age: "not a number", breed: "Persian" };
const obj = plainToInstance(CreateCatDto, invalidData);
const errors = await validate(obj);
// errors 배열:
[
{
property: 'name',
constraints: { isString: 'name must be a string' }
},
{
property: 'age',
constraints: { isInt: 'age must be an integer number' }
}
]
STEP 5: 왜 value를 반환하는가?
// 검증 통과 후
return value; // ❓ object가 아니라 value를 반환?
// 이유 1: value와 object는 데이터가 동일함
// - object는 검증을 위해 생성한 인스턴스
// - value는 원본 plain object
// - 둘 다 같은 데이터를 담고 있음
// 이유 2: 컨트롤러에서 받는 타입
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
// TypeScript는 여기서 createCatDto가 CreateCatDto 타입이라고 생각하지만
// 실제로는 plain object일 수도 있음 (런타임에서는 차이 없음)
}
// 개선 방법: object를 반환하도록 수정
return object; // 이렇게 하면 실제 인스턴스를 반환
4. 전체 실행 흐름 시각화
// 1️⃣ 클라이언트 요청
POST /cats
Body: { "name": "Kitty", "age": 3, "breed": "Persian" }
↓
// 2️⃣ NestJS가 요청 받음
const rawBody = { name: "Kitty", age: 3, breed: "Persian" };
// typeof rawBody: "object"
// rawBody instanceof CreateCatDto: false
↓
// 3️⃣ ValidationPipe.transform() 호출
transform(
value: { name: "Kitty", age: 3, breed: "Persian" },
metadata: { type: 'body', metatype: CreateCatDto, data: undefined }
)
↓
// 4️⃣ 검증 필요 여부 체크
if (!CreateCatDto || !toValidate(CreateCatDto)) // false, 계속 진행
↓
// 5️⃣ plain object → class instance 변환
const object = plainToInstance(CreateCatDto, value);
// object instanceof CreateCatDto: true ✅
// 이제 object에는 @IsString(), @IsInt() 메타데이터가 있음!
↓
// 6️⃣ 검증 실행
const errors = await validate(object);
// validate()가 메타데이터를 읽고 각 속성 검증
// errors.length === 0 이면 통과
↓
// 7️⃣ 검증 통과하면 반환
return value;
↓
// 8️⃣ 컨트롤러 메서드 실행
async create(createCatDto: CreateCatDto) {
// createCatDto 사용 가능
}
5. 흔한 오해와 해결
오해 1: "왜 TypeScript 타입만으로는 안 되나요?"
// TypeScript 타입은 컴파일 타임에만 존재
interface CreateCatDto {
name: string;
age: number;
breed: string;
}
// 컴파일 후 (JavaScript)
// interface는 완전히 사라짐! 런타임에 존재하지 않음
// 반면 클래스는 런타임에도 존재
class CreateCatDto {
name: string;
age: number;
breed: string;
}
// 컴파일 후에도 클래스는 남아있음
// 따라서 metatype으로 CreateCatDto 클래스 정보를 받을 수 있음!
오해 2: "plainToInstance 없이 바로 validate 하면 안 되나요?"
// ❌ 안 됨!
const plainObject = { name: "Kitty", age: 3, breed: "Persian" };
const errors = await validate(plainObject);
// errors는 빈 배열! 검증이 안 됨!
// 이유: plainObject에는 데코레이터 메타데이터가 없음
// validate()는 메타데이터를 읽어야 하는데 읽을 게 없음
// ✅ 반드시 plainToInstance 필요
const object = plainToInstance(CreateCatDto, plainObject);
const errors = await validate(object);
// 이제 object에 메타데이터가 있어서 검증 가능!
6. 개선된 코드 (더 명확한 버전)
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
const { metatype } = metadata;
// 1. metatype이 없거나 기본 타입이면 검증 불필요
if (!metatype || this.isNativeType(metatype)) {
return value;
}
// 2. plain object를 class instance로 변환
// 이 과정에서 데코레이터 메타데이터가 활성화됨
const instance = plainToInstance(metatype, value);
// 3. class-validator로 검증
const errors = await validate(instance);
// 4. 에러가 있으면 예외 발생
if (errors.length > 0) {
const messages = errors.map(error =>
Object.values(error.constraints || {})
).flat();
throw new BadRequestException({
message: 'Validation failed',
errors: messages
});
}
// 5. 변환된 인스턴스 반환 (원본 value가 아닌 instance!)
return instance;
}
private isNativeType(metatype: Function): boolean {
const nativeTypes = [String, Boolean, Number, Array, Object];
return nativeTypes.includes(metatype);
}
}
7. 핵심 요약
- 클라이언트 데이터는 plain object: 클래스 정보와 데코레이터 메타데이터가 없음
- plainToInstance의 역할: plain object → class instance 변환으로 메타데이터 활성화
- validate()의 요구사항: 반드시 클래스 인스턴스여야 메타데이터를 읽을 수 있음
- toValidate()의 역할: 기본 타입은 검증 불필요, 커스텀 클래스만 검증
- metatype의 중요성: TypeScript 타입 정보를 런타임에서 사용 가능하게 함
6. Built-in ValidationPipe 활용
NestJS가 제공하는 내장 ValidationPipe는 더 많은 기능을 제공합니다:
// 전역 적용
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 없는 속성 제거
forbidNonWhitelisted: true, // DTO에 없는 속성 있으면 예외
transform: true, // 자동 타입 변환
transformOptions: {
enableImplicitConversion: true,
},
disableErrorMessages: false, // 프로덕션에서 에러 메시지 숨김
}));
자세한 내용은 Validation 기법 문서를 참고하세요.
7. DefaultValuePipe 활용
쿼리 파라미터가 없을 때 기본값을 제공합니다:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe)
activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe)
page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
동작 순서:
- DefaultValuePipe가 먼저 실행되어 값이 없으면 기본값 제공
- ParseBoolPipe 또는 ParseIntPipe가 타입 변환 수행
8. 고급 사용 사례
8.1 Entity 변환 Pipe
@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
constructor(private userService: UserService) {}
async transform(id: string, metadata: ArgumentMetadata): Promise<UserEntity> {
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}
// 사용
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity; // 이미 DB에서 조회된 entity
}
이렇게 하면 핸들러 코드가 선언적이고 보일러플레이트 코드를 줄일 수 있습니다.
9. 주요 차이점 정리
구분 class-validator Zod
| 스타일 | 데코레이터 기반 | 스키마 기반 |
| 타입 추론 | 별도 DTO 클래스 필요 | z.infer로 타입 자동 추론 |
| 단일 소스 | DTO 클래스 자체 | 스키마에서 타입 생성 |
| 의존성 | class-transformer 필요 | 단독 사용 가능 |
| TypeScript | 필수 | 필수 (strictNullChecks) |
10. 베스트 프랙티스
- 전역 ValidationPipe 사용: 애플리케이션 전체에 일관된 검증 적용
- DTO 재사용: 같은 DTO를 여러 곳에서 사용하여 DRY 원칙 준수
- 명확한 에러 메시지: 사용자가 이해하기 쉬운 에러 메시지 제공
- 적절한 스코프 선택: 필요한 레벨에서만 Pipe 적용
- 변환과 검증 분리: 각 Pipe는 단일 책임을 가지도록 설계
- 비동기 처리: 필요시 async/await 활용 (DB 조회 등)
- Native 타입 스킵: JavaScript 기본 타입은 검증 생략
- 의존성 주입 활용: 모듈 내에서 전역 Pipe 등록 시 DI 사용 가능
반응형
'개발 공부 > nest.js' 카테고리의 다른 글
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[exception-filters] (1) | 2025.12.16 |
|---|---|
| [nest.js] NestJS 제공 라이브러리 및 데코레이터 완벽 정리 (0) | 2025.12.14 |
| [nest.js] Middleware vs Guards vs Interceptors (0) | 2025.12.14 |
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Middleware] (0) | 2025.12.14 |
| [nest.js] nest.js 뿌수기 공식 docs 모조리 파헤치기[Modules] (0) | 2025.12.11 |
Comments
