NestJS는 데코레이터를 활용하여 라우트, 파라미터, 메타데이터 등을 선언적으로 정의할 수 있는 기능을 제공한다. 데코레이터가 무엇인지, 그리고 실제 프로젝트에서 유용하게 사용할 수 있는 커스텀 데코레이터(Custom Decorator)를 만드는 과정을 소개한다.
데코레이터란?
데코레이터는 클래스, 메서드, 프로퍼티, 파라미터에 추가적인 동작을 부여하는 데 사용되는 특수한 함수다. NestJS는 TypeScript의 데코레이터를 기반으로 작동하며, 다음과 같은 목적에 사용할 수 있다:
- HTTP 요청 처리 간소화: 요청 객체에서 값을 추출하거나 특정 동작을 추가
- 코드 가독성 향상: 중복 코드를 줄이고, 선언적으로 로직을 정의
- 메타데이터 설정: 클래스나 메서드에 메타데이터를 추가해 다양한 동작을 구현
메타데이터(Metadata)
- 데이터를 설명하는 데이터
- NestJS에서는 데코레이터를 통해 클래스, 메서드, 파라미터 등에 메타데이터를 추가하고, 런타임에 이를 활용함 e.g.
@GET,@POST,@ApiTags,@Controller등
NestJS 기본 제공 데코레이터
NestJS에서 다음과 같은 기본 데코레이터를 제공한다. 이 데코레이터들은 Express(또는 Fastify)를 추상화하여 요청(req), 응답(res) 객체를 쉽게 다룰 수 있도록 돕는다.
| 데코레이터 | 설명 | 해당하는 Express 객체 |
|---|---|---|
@Body() |
요청 본문 데이터 | req.body |
@Param() |
경로 파라미터 | req.params |
@Query() |
쿼리 문자열 | req.query |
@Headers() |
요청 헤더 | req.headers |
@Req() |
요청 객체 전체 | req |
@Res() |
응답 객체 전체 | res |
커스텀 데코레이터 구현하기
커스텀 데코레이터란?
NestJS는 기본 제공 데코레이터 이외에도 커스텀 데코레이터를 만들어 프로젝트 요구사항에 맞는 동작을 정의할 수 있다. 예를 들어, 요청 객체에서 특정 프로퍼티를 추출하거나 검증하는 과정을 추가할 수 있다.
e.g. @GetUserUuid 데코레이터
다음은 인증된 사용자의 userUuid를 req 객체에서 추출하는 커스텀 데코레이터다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUserUuid = createParamDecorator(
(data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest(); // HTTP 요청 객체 가져오기
const user = request.user; // 인증된 사용자 정보
// userUuid가 없는 경우 에러 발생
if (!user || !user.userUuid) {
throw new Error('userUuid를 찾을 수 없습니다. 인증이 필요합니다.');
}
return user.userUuid; // userUuid 반환
},
);data는 데코레이터를 호출할 때 전달받는 값ctx는 요청 컨텍스트로, 요청 객체(req)를 가져올 수 있음data가 있으면 요청 객체의user속성에서 해당 필드만 반환하고, 없으면user객체 전체를 반환
컨트롤러에서 @GetUserUuid를 사용하여 간결하게 userUuid를 추출할 수 있다.
변경 전: req 객체에 직접 접근
@Get('all')
@ApiOperation({
summary: '사용자의 모든 일정 조회',
description: '사용자의 반복 일정을 포함한 전후 1년의 일정을 조회합니다.',
})
@ApiResponse({
status: 200,
description: '일정 조회 성공',
type: [ResponseScheduleDto],
})
async getAllSchedulesByUserUuid(@Req() req): Promise<ResponseScheduleDto[]> {
const userUuid = req.user.userUuid
return this.schedulesService.findAllByUserUuid(userUuid)
}변경 후: 커스텀 데코레이터 사용
@Get('all')
@ApiOperation({
summary: '사용자의 모든 일정 조회',
description: '사용자의 반복 일정을 포함한 전후 1년의 일정을 조회합니다.',
})
@ApiResponse({
status: 200,
description: '일정 조회 성공',
type: [ResponseScheduleDto],
})
async getAllSchedulesByUserUuid(
@GetUserUuid() userUuid: string,
): Promise<ResponseScheduleDto[]> {
return this.schedulesService.findAllByUserUuid(userUuid)
}결과:
- 요청이 들어오면
@GetUserUuid데코레이터가 req 객체에서userUuid를 추출해getAllSchedulesByUserUuid메서드의userUuid파라미터로 전달한다. - 코드의 가독성이 좋아지고 중복 코드가 줄어든다.
데코레이터 활용하기
1. 데이터 전달
커스텀 데코레이터는 팩토리 함수를 통해 데이터를 전달받아 동적으로 동작할 수 있다. 아래는 req 객체의 특정 프로퍼티를 추출하는 예시다.
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user; // 전달받은 data로 특정 속성 추출
},
);컨트롤러에서 사용
@Get('profile')
async getProfile(@User('name') name: string) {
console.log(`${name}님, 안녕하세요.`);
return { name };
}2. 다른 파이프와 조합
데코레이터는 파이프와 결합하여 값을 검증(validate)하거나 변환(transform)할 수 있다. validateCustomDecorators: true 옵션을 통해 ValidationPipe가 커스텀 데코레이터의 값을 검증하도록 설정
@Get('profile')
async getProfile(
@GetUserUuid(new ValidationPipe({ validateCustomDecorators: true }))
userUuid: string,
) {
console.log(`Validated User UUID: ${userUuid}`);
}3. 다른 데코레이터와 조합
applyDecorators를 사용하여 여러 데코레이터를 하나로 묶을 수 있다. 아래는 여러 API 문서화 데코레이터를 하나의 함수로 묶어 재사용성을 높인 코드다.
import { applyDecorators } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
export function ApiDocumentation(summary: string, description: string) {
return applyDecorators(
ApiOperation({ summary, description }), // API 설명
ApiResponse({ status: 200, description: '요청 성공' }), // 성공 응답
ApiResponse({ status: 400, description: '잘못된 요청' }), // 잘못된 요청
ApiResponse({ status: 500, description: '서버 에러' }), // 서버 에러
);
}
@Get('docs')
@ApiDocumentation('문서 조회', 'API 문서 정보를 조회합니다.')
getDocs() {
return 'API Documentation';
}
@Get('users')
@ApiDocumentation('사용자 조회', '모든 사용자를 조회합니다.')
getUsers() {
return 'User List';
}