NestJS/Docs_OVERVIEW

Custom route Decorators

흰제비갈매기 2024. 6. 7. 22:20

Custom route decorators

Nest decorator로 불리는 language feature로 build 됬다. Decorator는 programming language에서 흔히 사용되는 개념이다. 하지만 Javascript 세상에서는, 여전히 상대적으로 새로운 개념이다. 어떻게 decorator가 작동하는지 더 이해하기 위해서는, 다음을 기사를 추천한다.

ES2016 decorator는 함수를 반환하는 표현식이며, 대상, 이름, 속성 기술자를 인수로 받을 수 있다. 데코레이터를 적용하려면 데코레이터 앞에 '@' 문자를 붙이고 데코레이팅하려는 것의 맨 위에 배치한다. decorator는 class, method 또는 property를 위해 정의될 수 있다.



Param decorators

Nest는 HTTP route handler와 함께 사용할 수 있는 유용한 param decorator를 제공한다. 아래는 제공되는 Express, Fastify 객체를 쉽게 다를 수 있게 해주는 데코레이터 리스트이다.

@Decorator object description
@Reqeust(), @Req() req 전체 요청 객체(req)를 주입한다.
@Response(), @Res() res 전체 응답 객체(res)를 주입한다.
@Next() next next 함수를 주입한다. 미들웨어 함수에서 주로 사용된다.
@Session() req.session 세션 객체를 주입한다.
@Param(param?: string) req.params/ req.params[param] URL 파라미터를 주입한다. param 인자를 제공하면 해당 파라미터의 값을 가져오고, 제공하지 않으면 모든 파라미터를 객체 형태로 가져온다
@Body(param?:string) req.body/req.body[param] 요청 본문(body)을 주입한다. param 인자를 제공하면 해당 본문 필드의 값을 가져오고, 제공하지 않으면 본문을 객체 형태로 가져온다.
@Query(param?:string) req.query/req.query[param] 쿼리 문자열을 주입한다. param 인자를 제공하면 해당 쿼리 파라미터의 값을 가져오고, 제공하지 않으면 모든 쿼리 파라미터를 객체 형태로 가져온다.
@Headers(param?:string) req.headers/req.headers[param] 요청 헤더를 주입한다. param 인자를 제공하면 해당 헤더의 값을 가져오고, 제공하지 않으면 모든 헤드를 객체 형태로 가져온다.
@Ip() req.ip 요청의 IP주소를 주입한다.
@Query req.hosts 호스트 파라미터를 주입한다. 이는 요청의 호스트 정보를 가져온다.

추가적으로, custom decorators를 만들 수 있다.


node.js에서는 속성(properties)를 request object에 붙이는게 흔한 관행이다. 이런 속성을 route handler에서 extract할 수 있다.

const user = req.user;

code를 좀 더 readable하고 transparent하게 만들기 위해, @User() decorator를 만들고, 이를 controller 전반에 걸쳐 재사용할 수 있다.

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

만일 decorator의 기능이 조건적으로 이루어져야 한다면, 'data' 매개변수를 사용하여 데코레이터의 팩토리 함수에 인수를 전달할 수 있다. custom decorator를 사용하면 request object에서 특정 key에 해당하는 속성을 추출하여 route handler method에 주입할 수 있다. 이를 통해 코드의 가독성과 재사용성을 높일 수 있다. 예를 들어 authentication layer request의 유효성을 검사한다. 그리고 user entity에 request object를 붙인다. authenticated request의 user entity는 다음과 같을 것 이다.

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

name을 key로 갖는 decorator를 정의해보자. 이는 만일 존재한다면 관련된 value를 반환한다.(만일 존재하지 않거나 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;

    // user에서 data에 해당하는 정보가 있다면 그것을 반환
    // 그렇지 data가 지정되지 않았다면 user 객체를 반환
    return data ? user?.[data] : user;
  },
);

// 사용
import { Controller, Get } from '@nestjs/common';
import { User } from './user.decorator'; // 경로는 실제 파일 위치에 따라 다릅니다.

@Controller('profile')
export class ProfileController {
  @Get()
  getProfile(@User() user: any) {
    // 전체 사용자 객체 반환
    console.log(user);
    return user;
  }

  @Get('email')
  getUserEmail(@User('email') email: string) {
    // 사용자 이름만 반환
    console.log(email);
    return email;
  }
}

controller에서 @User() decorator를 통해 어떻게 특정한 property에 접근하는가?


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

위의 같은 decorator, 허나 다른 key로 다른 property에 접근할 수 있다.
사용자 객체가 깊거나 복잡한 경우, 이 방법을 통해 request handler 구현을 더 쉽게 만들고 가독성을 높일 수 있다.


<팁!>
Typescript 사용자는 createParamDecorator()가 제네릭이라는 점에 주목해야 한다.이를 통해 타입 안전성을 명시적으로 보장할 수 있다. (e.g. createParamDecorator((data, ctx)=> ... )) 또는 factory function에서 매개변수 타입을 지정할 수도 있다. (e.g. createParamDecorator((data:string, ctx)=> ...)) 만일 둘다 생략하면 data의 타입은 any이다.


factory function: 객체를 생성하는 함수. 객체의 생성과 초기화를 담당하며, 동일한 함수로 다양한 형태의 객체를 만들 수 있는 유연성을 제공한다.


Working with pipes

Nest는 기본 내장된 @Body(), @Param(), @Query()에서 처리하는 것과 같은 방식으로 custom param decorator를 처리한다.
이는 pipe는 custom annotated parameter에서도 실행된다는 걸 의미한다. (e.g. 위의 user argument ) 게다가, pipe를 바로 custom decorator에 apply할 수 있다. 그냥 custom decorator에도 pipe가 적용된다는 걸 뜻한다.

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

<팁!>
validateCustomDecorator 옵션은 반드시 true로 설정되어야 한다. ValidationPipe는 default로 custom decorator로 주석이 달린 인수를 검증하지 않는다.



Decorator composition

Nest는 mutiple decorator를 구성할 수 있도록 helper method를 제공한다. 예를 들어, authentication과 관련된 모든 decorator를 하나의 decorator로 combine하기를 원한다고 하자. 이는 다음과 같은 구성으로 완료된다.

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

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

이렇다면 다음과 같이 @Auth()decorator를 사용할 수 있다.

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

이렇게 함으로써 4개의 decorator를 하나의 선언으로 완료할 수 있다

<주의사항!>
@nestjs/swagger에서의 @ApiHidePropety() decorator는 조합할 수 없으며, applyDecorators 함수와 함께 제대로 작동하지 않는다.