icon
2장 : NestJS 핵심 개념 심화

커스텀 데코레이터 만들기


NestJS를 사용하면서 @Controller(), @Get(), @Body(), @Param() 등 다양한 데코레이터들을 접하셨을 겁니다. 이 데코레이터들은 특정 클래스, 메서드, 또는 매개변수에 메타데이터를 추가하거나 특정 기능을 부여하는 강력한 도구입니다. NestJS는 이러한 내장 데코레이터들 외에도 개발자가 직접 커스텀 데코레이터(Custom Decorators) 를 만들 수 있는 기능을 제공합니다.

커스텀 데코레이터는 코드의 중복을 줄이고, 가독성을 높이며, 관심사를 더욱 명확하게 분리하여 애플리케이션의 유지보수성을 향상시키는 데 크게 기여합니다. 이번 절에서는 커스텀 데코레이터의 개념을 이해하고, 실제로 만들어보고, 이를 활용하는 방법을 배워보겠습니다.


커스텀 데코레이터란?

커스텀 데코레이터는 NestJS의 createParamDecorator 헬퍼 함수를 사용하여 만듭니다. 이 함수는 파이프(Pipe)와 유사하게, 요청 컨텍스트(ExecutionContext)로부터 데이터를 추출하거나 변환하여 라우트 핸들러의 매개변수로 주입하는 역할을 합니다. 하지만 파이프가 주로 매개변수의 유효성 검사나 타입 변환에 초점을 맞춘다면, 커스텀 데코레이터는 요청으로부터 특정 데이터를 추출하는 데 더 유용하게 사용됩니다.

주요 사용 사례

  • 인증된 사용자 정보 추출: 요청 객체에 저장된 사용자(req.user) 정보를 직접 가져오기.
  • 특정 헤더 값 가져오기: Authorization 헤더나 커스텀 헤더 값을 간편하게 추출하기.
  • 세션 또는 쿠키 데이터 접근: 세션이나 쿠키에서 필요한 데이터를 가져오기.
  • 복잡한 요청 객체에서 특정 부분만 추출: req.query, req.params, req.body 등에서 원하는 데이터만 뽑아내기.

첫 번째 커스텀 데코레이터: @User() 만들기

가장 흔하게 사용되는 커스텀 데코레이터 중 하나는 현재 인증된 사용자 정보를 가져오는 데코레이터입니다. 예를 들어, JWT 인증을 통해 요청 객체에 사용자 정보가 req.user 형태로 저장되어 있다고 가정해 봅시다. 매번 req.user를 직접 가져오는 대신, @User() 데코레이터를 사용하여 간편하게 사용자 정보를 가져올 수 있도록 만들어보겠습니다.

단계 1: 데코레이터 파일 생성

src/common/decorators 디렉토리를 생성하고 user.decorator.ts 파일을 만듭니다.

src/common/decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    // ExecutionContext는 Http, RPC, Websockets 컨텍스트를 추상화합니다.
    // 여기서는 HTTP 요청을 다루므로 Http 컨텍스트로 전환합니다.
    const request = ctx.switchToHttp().getRequest();

    // data 인자는 데코레이터에 전달된 인자입니다 (예: @User('firstName')).
    // 만약 data가 있다면 해당 속성만 반환하고, 없다면 전체 user 객체를 반환합니다.
    return data ? request.user?.[data] : request.user;
  },
);

코드 설명

  • createParamDecorator((data, ctx) => { ... }): NestJS에서 매개변수 데코레이터를 생성할 때 사용하는 헬퍼 함수입니다. 인자로 콜백 함수를 받습니다.
  • data: unknown: 이 데코레이터를 사용할 때 전달하는 인자입니다. 예를 들어 @User('id')라고 사용하면 data'id'가 됩니다.
  • ctx: ExecutionContext: 현재 실행 컨텍스트에 대한 정보를 제공하는 객체입니다. HTTP 요청뿐만 아니라 WebSockets, gRPC 등 다양한 컨텍스트에서 작동할 수 있도록 추상화되어 있습니다.
  • ctx.switchToHttp().getRequest(): ExecutionContext를 HTTP 컨텍스트로 전환하고, Request 객체를 가져옵니다. Express.js의 req 객체와 동일하다고 생각하시면 됩니다.
  • request.user: 이 예시에서는 인증 과정에서 req.user에 사용자 정보가 담긴다고 가정합니다. 실제 애플리케이션에서는 가드나 미들웨어에서 이 정보를 request 객체에 추가하는 로직이 필요합니다.
  • data ? request.user?.[data] : request.user;: data 인자가 있으면 (예: id), request.user.id를 반환하고, data 인자가 없으면 (@User()), request.user 전체 객체를 반환합니다. ?. (Optional Chaining)을 사용하여 request.user가 없을 때 undefined를 반환하도록 안전하게 처리합니다.

커스텀 데코레이터 사용하기

이제 @User() 데코레이터를 컨트롤러에서 사용하여 간편하게 사용자 정보를 가져와 보겠습니다. 이 데코레이터가 작동하려면, 요청이 컨트롤러에 도달하기 전에 request.user 객체에 실제 사용자 정보가 채워져 있어야 합니다. 이를 위해 간단한 미들웨어를 사용하여 req.user를 임시로 설정해 보겠습니다. 실제 환경에서는 인증 가드(Guard)에서 JWT 토큰 등을 검증하여 사용자 정보를 설정하게 됩니다.

단계 1: 사용자 정보를 설정할 미들웨어 또는 가드 준비 (예시)

src/auth/auth.middleware.ts (예시)
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 실제 인증 로직 (DB 조회, JWT 검증 등)
    // 여기서는 임시로 가짜 사용자 정보를 req.user에 할당합니다.
    (req as any).user = { userId: 1, username: 'nest_user', roles: ['admin'] };
    console.log('User mocked by AuthMiddleware:', (req as any).user);
    next();
  }
}
src/app.module.ts (미들웨어 적용)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthMiddleware } from './auth/auth.middleware'; // 미들웨어 임포트

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes('*'); // 모든 경로에 AuthMiddleware 적용
  }
}

단계 2: 컨트롤러에서 @User() 데코레이터 사용

src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from './common/decorators/user.decorator'; // 커스텀 데코레이터 임포트

// 사용자 정보의 타입을 정의해두면 타입 안정성을 높일 수 있습니다.
interface CurrentUser {
  userId: number;
  username: string;
  roles: string[];
}

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('profile')
  // @User() 데코레이터를 사용하여 요청 객체에서 user 정보를 가져옵니다.
  getProfile(@User() user: CurrentUser) {
    console.log('Current User:', user);
    return `Hello, ${user.username}! Your ID is ${user.userId} and roles are ${user.roles.join(', ')}.`;
  }

  @Get('username')
  // @User('username') 데코레이터를 사용하여 user 객체에서 특정 속성만 가져옵니다.
  getUsername(@User('username') username: string) {
    console.log('Current Username:', username);
    return `Your username is: ${username}`;
  }
}

실행 및 확인

npm run start:dev 명령어로 애플리케이션을 실행합니다.

웹 브라우저나 Postman 등으로 http://localhost:3000/profile 또는 http://localhost:3000/username으로 GET 요청을 보냅니다.

콘솔에 Current User: { userId: 1, username: 'nest_user', roles: [ 'admin' ] }와 같은 메시지가 출력되고, 응답으로 사용자 정보가 나타나는 것을 확인할 수 있습니다.


고급 활용: 데코레이터와 가드의 결합

@User() 데코레이터는 가드와 함께 사용될 때 더욱 강력해집니다. 인증 가드가 요청을 통과시키면서 req.user에 사용자 정보를 설정하면, 컨트롤러에서는 @User() 데코레이터를 통해 이 정보를 깔끔하게 가져다 쓸 수 있기 때문입니다.

예를 들어, 역할 기반 접근 제어(RBAC)를 구현할 때, @Roles('admin')과 같은 커스텀 데코레이터를 만들고, 이 데코레이터가 설정한 메타데이터를 가드에서 읽어 사용자의 역할을 검증하는 방식으로 활용할 수 있습니다.

예시: @Roles() 데코레이터와 RolesGuard (개념만 소개)

src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
src/auth/guards/roles.guard.ts (간략화된 예시)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; // Reflector를 사용하여 메타데이터를 읽습니다.

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true; // 역할 제한이 없으면 접근 허용
    }
    const { user } = context.switchToHttp().getRequest();
    // 실제 로직: user의 roles가 requiredRoles에 포함되는지 확인
    return user.roles.some((role: string) => requiredRoles.includes(role));
  }
}
src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guards/auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';

@Controller('admin')
@UseGuards(AuthGuard, RolesGuard) // AuthGuard 먼저, RolesGuard 나중에 실행
export class AdminController {
  @Get('dashboard')
  @Roles('admin') // 이 메서드는 'admin' 역할만 접근 가능
  getAdminDashboard() {
    return 'Welcome to the admin dashboard!';
  }
}

이처럼 커스텀 데코레이터는 코드의 재사용성을 높이고, 특정 로직을 선언적으로 표현할 수 있게 하여 NestJS 애플리케이션의 구조를 더욱 깔끔하고 효율적으로 만들어줍니다.


이번 절을 통해 NestJS의 커스텀 데코레이터를 만들고 활용하는 방법을 학습하셨습니다. 앞으로 여러분의 NestJS 프로젝트에서 반복되는 로직이나 데이터 추출 작업을 커스텀 데코레이터로 추상화하여 코드의 품질을 높여보시길 바랍니다.

다음 장에서는 NestJS 애플리케이션의 핵심 기능인 데이터베이스 연동에 대해 자세히 알아보겠습니다.