NestJS 帶你飛! 시리즈 번역 14# Custom Decorator

2023. 6. 7. 19:05개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.

 

데코레이터(Decorator, 裝飾器)는 디자인 패턴중 하나로, 일부 프로그래밍 언어에서 이 디자인 패턴을 구현할 수 있습니다.

최근 TypeScript와 Javascript에서도 해당 기능을 추가하였으며, Nest는 데코레이터를 최대한 활용하여 기능을 쉽게 적용할 수 있도록 지원합니다. 개발속도, 가독성을 고려한다면 데코레이터는 유용한 도구가 아닐수 없습니다.

Custom Decorator

Nest는 많은 데코레이터를 지원하는데 특정 상황에선 내장 데코레이터를 사용하여도 해결이 되지 않는 상황이 발생할 수 있습니다. 이를 위해 Nest는 Custom Decorator (커스텀 데코레이터, 自訂裝飾器)기능을 지원합니다.

커스텀 데코레이터의 종류는 3가지로 분류할 수 있습니다.

파라미터 데코레이터

특정 자료들은 내장 데코레이터를 통해 얻어오지 못할 수도 있습니다.

ex: 인증/권한 부여에서 가져온 데이터

Express에 익숙하다면 아래의 코드를 써보신 기억이 있을겁니다.

그렇다면 사용자 정의 데이터가 왜 요청 객체에 저장이 되는걸까요?
이는 Middleware를 통해 확장되어, 해당 인증/권한 부여에선 일반적으로 쓰이는 방법이기 때문입니다.

const user = req.user;

한번 생각해봅시다.
내장 데코레이터를 통해선 어떻게 받아와야 할까요?

@Request 데코레이터를 통해 먼저 요청 객체를 받아오고, 이 요청 객체로부터 값을 받아오면 되는데요.

이런 방식이 나쁘다는건 아닙니다. 하지만 이 동작을 하나의 데코레이터로 지정하여 데코레이터만 붙여

값을 받아올수 있다면 더 편하지 않을까요?

Decorator는 CLI를 통해 생성이 가능합니다.

$ nest generate decorator <DECORATOR_NAME>
주의: <DECORATOR_NAME>은 경로를 포함할 수 있습니다. ex: decorators/user
경로를 포함하여 생성시 src 폴더 안에 경로를 포함하여 Decorator가 생성됩니다.

여기서 Userdecorators 폴더 아래에 생성해보겠습니다.

$ nest generate decorator decorators/user

src 폴더 아래에 decorators 라는 폴더와 함께 user.decorator.ts가 들어있습니다.

생성된 데코레이터의 뼈대는 아래와 같습니다. SetMetadata라는 함수가 하나 보입니다.

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

export const User = (...args: string[]) => SetMetadata('user', args);

그러나 파라미터 데코레이터는 SetMetadata를 사용하지는 않습니다.

createParamDecorator를 사용하며, 이 createParamDecorator를 통해 파라미터 데코레이터를 생성하고

Callback 안에 ExecutionContext를 통해 요청객체를 받아오고, 해당 요청 객체를 통해 원하는 자료를 가져옵니다.

아래는 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;
  },
);

User 데코레이터를 완성되었습니다. Middleware 하나를 더 만들어 user를 요청 객체에 추가하도록 하겠습니다.

위와 같이 CLI를 통해 생성해보겠습니다.

$ nest generate middleware middlewares/add-user

add-user.middleware.ts의 내용을 아래와 같이 수정해봅시다.

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class AddUserMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    req.user = { name: 'WOO' };
    next();
  }
}

다음으로 AppModule에서 AddUserMiddleware를 적용시켜봅시다.

이렇게되면 user를 요청 객체에 담을 수 있게 됩니다.

다음으로 User 데코레이터를 통해 user의 내용을 받아옴과 동시에 클라이언트에게 넘겨주도록 하겠습니다.

아래는 app.controller.ts의 예제입니다.

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

@Controller()
export class AppController {
  constructor() {}

  @Get()
  getHello(@User() user: any): string {
    return user;
  }
}

http://127.0.0.1:3000 에 접속하면 아래와 같은 결과를 확인할 수 있습니다.

@Param('id')와 같이 특정 자료만 가져오고 싶다면 어떻게 설계해야 할까요?

createParamDecorator 안의 Callback에 ExecutionContext 외에 data라는게 존재합니다.

사실 이 data는 데코레이터 안의 파라미터이므로, 해당 data를 통해 사용자 데이터를 추출해야 합니다.
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;
  },
);

app.controller.ts를 통해 user안의 name을 얻어오겠습니다.

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

@Controller()
export class AppController {
  constructor() {}

  @Get()
  getHello(@User('name') name: string): string {
    return name;
  }
}

http://127.0.0.1:3000 에 접속하면 아래와 같은 결과를 확인할 수 있습니다.

커스텀 Metadata 데코레이터

가끔은 일부 메서드에 대한 Metadata를 설정해야 할 때가 있을겁니다.

ex: 역할 기반 접근제어 (RBAC, 角色權限控管)

이때 Metadata를 설정하여 특정 역할만이 해당 메서드에 접근할수 있도록 할 수 있습니다.

간단한 역할 기반 제어기능을 통해 어떤 뜻인지 알아봅시다.

CLI를 통해 Roles를 생성할 수 있습니다.

$ nest generate decorator decoractors/roles

생성된 뼈대가 바로 커스텀 MetaData 데코레이터의 양식입니다.

SetMetadata가 바로 MetaData 데코레이터 입니다.

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

export const Roles = (...args: string[]) => SetMetadata('roles', args);

이 예제의 의미는 다음과 같습니다.

Rols를 데코레이터이며, @Roles('admin')을 사용하여
admin 문자열을 데코레이터에 전달한 후 SetMetadata를 사용하여
roleskey 값으로 설정하고 ['admin']을 해당 값으로 지정하여 Metadata를 설정합니다.

다음으로 RoleGuard를 설정하여 역할 기반 제어를 모방해보겠습니다.

$ nest generate guard guards/role

Metadata의 값을 얻어오기 위해선 반드시 Nest가 제공하는 Reflector를 사용해야 합니다.

의존성 주입을 사용하여 Reflector를 가져오고
get(metadataKey: any, target: Function | Type<any>)로 가져올 Metadata를 지정합니다. 이중 metadataKey는 지정할 Metadata Key를 getHandler에는 target을 입력하면 됩니다.

role.guard.ts의 내용을 수정해보겠습니다.

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

@Injectable()
export class RoleGuard implements CanActivate {

  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return this.matchRoles(roles, user.roles);
  }

  private matchRoles(resources: string[], target: string[]): boolean {
    return !!resources.find(x => target.find(y => y === x));
  }
}

RolesRoleGuard의 정의가 완료되었습니다.

마지막으로 AddUserMiddleware의 내용을 수정하여 staffuser를 추가해봅시다.

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class AddUserMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    req.user = { name: 'WOO', roles: ['staff'] };
    next();
  }
}

마지막으로 app.controller.ts를 수정하여 getHelloadmin의 권한이 있는 사람만 엑세스 할 수 있도록 설정했습니다.

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './decorators/roles/roles.decorator';
import { User } from './decorators/user/user.decorator';
import { RoleGuard } from './guards/role/role.guard';

@Controller()
export class AppController {
  constructor() {}

  @UseGuards(RoleGuard)
  @Roles('admin')
  @Get()
  getHello(@User('name') name: string): string {
    return name;
  }
}

http://127.0.0.1:3000에 접속해보면...

위와 같이 값을 받아올수 없게됩니다. (RoleGuard에 막힌 모습입니다.)

 

 

데코레이터 합쳐 사용하기

일부 데코레이터는 서로 관련이 있을 수 있습니다.

ex: 인증을 위해 가드를 사용하고 사용자 Metadata를 추가해야 할 경우

이 경우 각각의 데코레이터를 반복적으로 사용하는 대신

Nest에서는 applyDecorators라는 함수로

여러 데코레이터를 묶어 하나의 데코레이터로 통합하는 기능을 제공합니다.

각각 기능을 구현할 때마다 해당 합쳐진 데코레이터를 사용하면 됩니다. 아래에 인증/권한 검사를 진행하는 간단한 통합 데코레이터를 만들어 보겠습니다.

먼저 CLI를 통해 Auth Decorator를 생성합니다.

$ nest generate decorators/auth

 

주의: 이번 포스팅은 데코레이터를 합치는 기능에 초점을 두기 위해
AuthGuard의 내용은 수정하지 않습니다. true만 반환하도록 설정 하면 되니까요.

AuthGuard를 생성한 후에 auth.decorator.ts를 수정하겠습니다.

applyDecoratorsUseGuardsRoles 를 합쳐 하나의 데코레이터로 사용하겠습니다.

import { applyDecorators, UseGuards } from '@nestjs/common';
import { Roles } from '../roles/roles.decorator';
import {AuthGuard} from '../../guards/auth/auth.guard';
import {RoleGuard} from '../../guards/role/role.guard';
export const Auth = (...roles: string[]) => applyDecorators(
	Roles(...roles),
	UseGuards(AuthGuard, RoleGuard)
);

 

 

마지막으로 app.controller.ts를 수정합니다.

Auth 데코레이터를 적용하여 getHello 메서드는 staff만이 접근가능하게 설정하겠습니다.

마치며

Custom Decorator는 Nest가 지원하는 내장 데코레이터의 부족한 부분을 채워줄 수 있으며

매우 유연한것이 특징입니다.

아래는 오늘 배운 내용의 요약본입니다.

1. Custom Decorator는 : 파라미터 데코레이터, 커스텀 Metadata 데코레이터, 통합 데코레이터의 구현이 가능합니다.

2. 파라미터 데코레이터는 createParamDecorator를 통해 만들 수 있습니다.

3. 커스텀 MetaData 데코레이터는 SetMetadata를 통해 확장이 가능합니다.

4. 통합 데코레이터는 applyDecorators를 통해 만들 수 있습니다.

 

Nest의 기본 기능과 사용 방식에 대해 소개를 마쳤습니다.

다음 포스팅에서부터는 심화 기능에 대해 배워 볼겁니다. 기대해주세요!