1. Custom decorators
Nest는 데코레이터라는 언어 기능을 중심으로 구축되었습니다. 데코레이터는 일반적으로 사용되는 많은 프로그래밍 언어들에서 잘 알려진 개념이지만 자바스크립트 세계에서는 아직까진 비교적 생소한 개념입니다. 데코레이터의 작동 방식을 더 잘 이해하려면 이 글을 읽어보시기 바랍니다.
다음은 데코레이터에 대한 간단한 정의입니다:
ES2016 데코레이터는 함수를 반환하고 target, name 및 property descriptor를 인자로 받을 수 있는 표현식입니다. 데코레이터 앞에 '@' 문자를 붙이고 데코레이터를 적용하려는 대상의 맨 위에 배치합니다. 데코레이터는 클래스, 메서드 또는 프로퍼티에 대해 정의할 수 있습니다.
1-1. Param decorators
Nest는 HTTP 라우트 핸들러와 함께 사용할 수 있는 유용한 매개변수 데코레이터 세트를 제공합니다. 다음은 Nest가 제공하는 데코레이터와 이들이 나타내는 일반 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[param] |
@Ip() |
req.ip |
@HostParam() |
req.hosts |
이러한 제공 데코레이터 이외에도 우리는 이 장의 메인 주제인 커스텀 데코레이터를 만들 수가 있는데요.
그런데 커스텀 데코레이터는 왜 필요할까요?
node.js 세계에서는 request 객체에 property를 첨부하는 것이 일반적인 관행입니다. 그런 다음, 아래와 같은 코드를 사용하여 각 라우트 핸들러에서 property를 수동으로 추출하게 됩니다:
const user = req.user;
코드를 더 읽기 쉽고 분명하게 만들기 위해 @User() 데코레이터를 만들어 모든 컨트롤러에서 재사용할 수 있게 해봅시다.
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);
}
이렇게 함으로써 라우트 핸들러에서 @Req를 사용하지 않고도 req 객체의 user 속성을 받게 됩니다.
1-2. Passing data
데코레이터의 동작이 특정 조건에 따라 달라지는 경우 data 매개변수를 데코레이터의 팩토리 함수에 인자로 전달할 수 있습니다.
이에 대한 한 가지 사용 사례로, key 별로 request 객체에서 property를 추출하는 커스텀 데코레이터가 있습니다. 예를 들어, authentication layer에서 request의 유효성을 검사하고 User 엔티티를 request 객체에 첨부한다고 가정해 보겠습니다.
이때, 인증된 요청의 User 엔티티는 다음과 같을 것입니다:
{
"id": 101,
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@email.com",
"roles": ["admin"]
}
여기서 이제 속성 이름을 key로 사용하고, 해당 속성이 존재하면 관련된 value를 반환하는 데코레이터를 정의해 보겠습니다 (존재하지 않거나 user 객체가 생성되지 않은 경우 정의되지 않음).
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;
},
);
그러면 아래처럼 @User() 데코레이터를 통해 컨트롤러의 특정 속성에 액세스할 수 있게됩니다.
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`);
}
동일한 데코레이터를 다른 key와 함께 사용하여 다른 속성에 액세스할 수 있습니다. user 객체가 깊거나 복잡한 경우 요청 핸들러를 더 쉽고 가독성 있게 구현할 수 있습니다.
TypeScript 사용자의 경우 createParamDecorator<T>()는 제네릭이라는 점에 유의해야 합니다. 즉, createParamDecorator<string>((data, ctx) => ...)와 같이 명시적으로 type safety를 적용할 수 있습니다.
또는 팩토리 함수에서 매개변수 타입을 지정할 수도 있습니다(예: createParamDecorator((data: string, ctx) => ...). 만약 둘 다 생략한다면 데이터 유형은 any 타입이 됩니다.
1-3. Working with pipes
Nest는 커스텀 매개변수 데코레이터를 기본 제공 매개변수(@Body(), @Param() 및 @Query())들과 동일한 방식으로 처리합니다.
즉, 커스텀 annotation이 달린 매개변수(예제에서는 사용자 인수)에 대해서도 파이프가 실행될 수 있는 것입니다. 또한 커스텀 데코레이터에 직접 파이프를 적용할 수도 있습니다:
@Get()
async findOne(
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
console.log(user);
}
validateCustomDecorator 옵션을 참으로 설정해야 합니다. ValidationPipe는 아무 설정을 하지 않으면 커스텀 데코레이터로 주석이 달린 인자의 유효성을 검사하지 않습니다.
1-4. Decorator composition
Nest는 여러 데코레이터를 구성하는 헬퍼 메서드를 제공합니다. 예를 들어, 인증과 관련된 모든 데코레이터를 하나의 데코레이터로 결합하고 싶다고 가정해 보겠습니다. 그 경우 다음과 같은 구성으로 이를 수행할 수 있습니다:
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가지의 데코레이터를 한 번에 모두 적용하는 효과를 가져오게 됩니다.