NestJS 공식문서 번역 겸 공부하는 글 입니다.
의역 및 오역이 있을 수 있습니다.
https://docs.nestjs.com/custom-decorators

Custom decorators

Custom router decorators

Nest는 데코레이터라는 특성을 중심으로 만들어졌다. 데코레이터는 많은 프로그래밍 언어에서 알려진 개념이지만, 자바스크립트 세계에서는 상대적으로 새로운 편이다. 데코레이터가 어떻게 작동하는지 더 잘 이해하기 위해서는, 이 글을 읽어보는 것을 추천한다. 아래는 간단한 정의이다.

ES2016 데코레이터는 함수를 반환하고, 인자로 target, name, property, description을 받을 수 있는 expression이다. decorate하려고하는 것 맨 위에 @를 접두어로 붙인 데코레이터를 위치함으로써 데코레이터를 적용할 수 있다. 데코레이터는 class, method, property에 대해 정의 할 수 있다.

Param decorator

Nest는 HTTP route handler와 함께 사용할 수 있는 유용한 param decorator들을 제공한다. 아래는 제공되는 데코레이터들과 그들이 나타내는 express(또는 fastify) 오브젝트이다.

@Request(), @Req() req
@Response(), @Res() res
@Next() next
@Session req.session
@Param(param?: string) req.params / req.params[param]
@Body(param?: string req.body / req.body[param]
@Query(param?: string req.query / req.query[param]
@Headers(param?: string) req.headers / req.headers[params]
@Ip() req.ip
@HostParam() req.hosts

node.js에서 request 오브젝트에 프로퍼티를 추가하는 것은 일반적이다. 그리고 각각의 route handler에서 아래와 같이 추가된 프로퍼티를 추출해 사용할 수 있다.

const user = req.user;

코드를 더욱 가독성있고 투명하게 만들기 위해, @User() 데코레이터를 만들고 재사용하는 방법을 사용할 수 있다.

// user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

그 후, 필요한 곳 어디든 간단하게 사용할 수 있다.

@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}

Passing data

데코레이터가 특정 조건에 의존하여 작동한다면, data 파라미터를 데코레이터의 팩토리 함수에 넘겨줄 수 있다. 하나의 사례는 request 오브젝트로부터 프로퍼티를 추출하는 데코레이터이다. 예를 들어, 우리의 authentication layer가 request를 validation하고 user entity를 request 오브젝트에 추가하였다고 가정해보자. user entity는 아래와 같은 형태이다.

{
  "id": 101,
  "firstName": "Alan",
  "lastName": "Turing",
  "email": "alan@email.com",
  "roles": ["admin"]
}

프로퍼티명을 key값으로 받아서 값이 존재한다면 리턴해주는 데코레이터를 정의해보자(user object가 만들어지지 않았거나 해당 프로퍼티가 존재하지 않으면 undefined를 리턴할 것이다).

// user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    return data ? user?.[data] : user;
  },
);

그리고 controller에서 @User() 데코레이터를 통해 아래와 같이 특정 프로퍼티에 접근 가능하다.

@Get()
async findOne(@User('firstName') firstName: string) {
  console.log(`Hello ${firstName}`);
}

다른 key값을 사용하여 다른 프로퍼티에도 접근 가능하다. user 오브젝트가 deep하거나 복잡한 경우 이 방식이 좀 더 가독성 있는 request handler 구현을 도와줄 것이다.

[hint]
타입스크립트 사용자를 위해, createParamDecorator()는 제네릭임을 주의하라. 이것은 type safety를 강화할 수 있음을 의미한다(예를 들어, createParamDecorator((data, ctx) => …)). 또는 팩토리 함수에 특정 파라미터의 타입을 지정할 수도 있다(createParamDecorator((data: string, ctx) => …)). 만약 둘 다 생략한다면, data의 타입은 any 이다.

Working with pipes

Nest는 @Body(), @Query(), @Param() 처럼 내장된 데코레이터들과 같은 방식으로 custom param decorator를 취급한다. 이것은 custom annotated parameter에도 pipe가 작동한다는 것을 의미한다. 또한, pipe를 직접 custom decorator에 적용할 수도 있다.

@Get()
async findOne(
  @User(new ValidationPipe({ validateCustomDecorators: true }))
  user: UserEntity,
) {
  console.log(user);
}

[hint]
validateCustomDecorators 옵션이 true임에 주목하자. ValidationPipe는 기본적으로 커스텀 데코레이터로 주석된 인자는 validation하지 않는다.

Decorator composition

Nest는 multiple decorator의 구성을 도와주는 helper method를 제공한다. 예를 들어, authentication에 관련된 모든 데코레이터를 하나의 데코레이터로 합치고 싶다고 가정해보자. 이것은 아래의 예제처럼 구성할 수 있다.

// auth.decorator.ts

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

그리고 이 커스텀 @Auth() 데코레이터를 사용하면 된다.

@Get('users')
@Auth('admin')
findAllUsers() {}

하나의 데코레이터 선언으로 4개의 데코레이터가 적용되었다.

[warning]
@nestjs/swagger 패키지에서 제공되는 @ApiHideProperty() 데코레이터는 applyDecorators() method에서 작동하지 않으며, 하나로 묶을 수 없다.