NestJS - Custom Parameter Decorator


이 포스트는 NestJS 에서 라우트 핸들러의 매개변수에 적용할 수 있는 매개변수 데커레이터를 커스텀하여 활용하는 법에 대해 알아본다.

소스는 example 에 있습니다.


1. Decorator

1.1. 내장 Decorator 종류

NestJS 는 ES6 에 도입된 Decorator 를 적극 활용한다.

아래는 제공되고 있는 내장 Decorator 와 그에 대응되는 Express(or Fastify) 의 객체이다.

내장 DecoratorExpress 객체
@Request(), @Req()req
@Response(), @Res()res
@Next()next
@Session()req.session
@Param(param?: string)req.param / 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

2. Custom Parameter Decorator

NestJS 는 라우트 핸들러의 매개변수에 적용할 수 있는 매개변수 데커레이터를 제공한다.

Parameter Decorator 에 대한 좀 더 상세한 내용은 Decorator: Blog 을 참고하세요.

NestJS - Guard, JWT 에서 인증/인가 Guard 로 처리하는 것을 권장했는데 로그인할 때 발급받은 JWT 를 요청마다 헤더에 포함하고 Guard 에서 이 JWT 를 검증(인증) 해서 얻은 유저 정보를 가지고 지금 수행하려는 요청을 이 유저가 실행할 수 있는지 검사(인가) 하는 과정을 거친다.

이 과정에서 라우터 핸들러에 전달된 요청 객에체 유저 정보를 추가로 실어서 이후에 이용하는 방법을 많이 이용하는데 이 때 @User() 라는 Custom Parameter Decorator 를 만들어서 유저 정보를 추출해보도록 하자.

NestJS - Guard, JWT3.4. Guard 로 인가 처리 에서 생성했던 AuthGuard 를 다시 구현해보자.

JWT 검증하는 부분은 생략하고 요청 객체에 유저 정보를 하드코딩하여 설정한다.

/src/auth.guard.ts

import { CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }

  // 얻은 정보(request) 를 내부 규칙으로 평가 진행
  private validateRequest(request: any) {
    // JWT 를 검증해서 얻은 정보 넣음. 편의상 하드코딩함
    request.user = {
      name: 'assu',
      email: 'test@test.com',
    };
    return true;
  }
}

컨트롤러로 전달된 요청 객체에서 유저 정보를 얻는다.

/src/app.controller.ts

@Get()
getHello(@Req() req): string {
  console.log(req.user);
  return this.appService.getHello();
}

하지만 req.user 를 메서드 내부에서 직접 쓰는 것이 아니라 아래와 같이 @User() 데커레이터와 함께 인수로 직접 받도록 해보자.

/src/app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from './user.decorator';

interface User {
  name: string;
  email: string;
}
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@User() user: User): string {    // User 커스텀 데커레이터 사용
    console.log('------', user);
    return this.appService.getHello();
  }
}

/src/user.decorator.ts

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

export const User = createParamDecorator(
        // createParamDecorator 를 이용하여 User Decorator 선언
        (data: unknown, ctx: ExecutionContext) => {
          const request = ctx.switchToHttp().getRequest(); // 실행 콘텍스트에서 요청 객체 얻어옴
          return request.user; // AuthGuard 에서 설정한 유저 객체 반환, req.user 가 타입이 any 였다면 User 라는 타입을 갖게 됨
        },
);
$ npm run start:dev

$ curl --location 'http://localhost:3000' | jq

------ { name: 'assu', email: 'test@test.com' }

3. Custom Parameter Decorator 의 data 활용

이제 createParamDecorator() 의 첫 번째 인수인 data 를 사용해보자.

data 는 데커레이터 선언 시 인수로 넘기는 값인데 @UserData('name) 과 같이 이름만 가져와서 매개변수로 받고 싶거나 @UserData() 처럼 인수로 아무것도 넘기지 않으면 유저 객체 모두를 넘겨받고 싶을 때 활용할 수 있다.

/src/user-data.decorator.ts

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

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

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

/src/app.controller.ts

  @Get('/name')
getHello2(@UserData('name') name: string): void {
  console.log('------', name);
}

@Get('/name3')
getHello3(@UserData() name: string): void {
  console.log('------', name);
}
$ curl --location 'http://localhost:3000/name'

------ assu


$ curl --location 'http://localhost:3000/name3'

------ { name: 'assu', email: 'test@test.com' }

4. 유효성 검사 파이프(ValidationPipe) 적용

이제 ValidationPipe 를 같이 적용하여 매개 변수의 유효성 검사를 해보도록 한다.

ValidationPipe 에 대한 상세한 내용은 NestJS - Pipe, Validation 을 참고하세요.

먼저 오류를 발생시키기 위해 AuthGuard 의 name 에 1 를 설정해본다.

app.guard.ts

private validateRequest(request: any) {
  // JWT 를 검증해서 얻은 정보 넣음. 편의상 하드코딩함
  request.user = {
    name: 1,
    email: 'test@test.com',
  };
  return true;
}

그리고 class-validator 를 적용하기 위해 interface 대신 클래스로 변경한다.

$ npm i class-validator class-transformer

/src/app.controller.ts

import { Controller, Get, ValidationPipe } from '@nestjs/common';
import { User } from './user.decorator';
import { UserData } from './user-data.decorator';
import { IsString } from 'class-validator';

class UserEntity {
  @IsString()
  name: string;

  @IsString()
  email: string;
}
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
...
  @Get('/with-pipe')
  getHello4(
    @User(new ValidationPipe({ validateCustomDecorators: true })) user: UserEntity): void {
    console.log('---', user);
  }
}
$ curl --location 'http://localhost:3000/with-pipe' | jq

{
  "statusCode": 400,
  "message": [
    "name must be a string"
  ],
  "error": "Bad Request"
}

5. Decorator 합성

applyDecorator 헬퍼 메서드를 이용하여 여러 데커레이터를 하나로 합성할 수 있다.

Decorator 합성에 대한 좀 더 상세한 내용은 Decorator2. Decorator Composition (데커레이터 합성) 을 참고하세요.

아래는 SetMetadata, UseGuards, ApiBearerAuth, ApiUnauthorizedResponse 라는 데커레이터를 하나로 합쳐서 Auth 데커레이터로 합성하는 예시이다.

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

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

참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 한용재 저자의 NestJS로 배우는 백엔드 프로그래밍을 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10