본문 바로가기

데일리

[23.10.21~22] nestjs decorator

글의 목적

  • 원래는 클래스에 대해서 글을 작성해야 하는데 몸살기운이 있어서 집중이 안되기 때문에 조금은 간단한 실험이자 nestjs를 공부하면서 nestjs를 잘 다루기 위해서는 decorator를 잘 다뤄야 한다는 느낌을 받았기 때문에 간단한 실험을 기록하려고 한다.
  • 초보자도 복붙하고 명령어를 따라하면 바로 실행해볼 수 있도록 모든 명령어와 코드를 제공할 예정.

본론

1. 폴더 생성 후 npm init, ts 초기 셋업

  1. vscode를 열어서 아무 폴더를 생성해줍니다.
  2. 저는 test라는 폴더를 만들었습니다. test 폴더로 들어가줍니다.
  3. npm 패키지관리자를 설치해줍니다. (node는 설치가 되어 있다는 가정입니다.)
  4. 타입스크립트도 설치해줍니다.
npm init -y
npm i typescript, ts-node
  1. deco.ts 파일을 만들어 줍니다.
  2. tsconfig.json 파일을 만들고 아래 내용을 넣어줍니다.
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}
  1. .ts파일을 테스트를 하기 위한 사전 준비를 모두 마쳤습니다.

2. decorator 생성

  • 일단 간단하게 decorator가 어떻게 동작하는지 부터 알아보겠습니다.
// deco.ts
function deco(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log("데커레이터가 평가됨.");
}

class TestClass {
  @deco
  test() {
    console.log("함수가 호출됨");
  }
}

const t = new TestClass();
t.test();
  • 위에 코드를 붙여넣기 하시고 아래 명령어를 실행해줍니다.(node환경에서 .ts 파일이 실행됩니다.)
ts-node deco.ts
// 출력
데커레이터가 평가됨
함수가 호출됨
  • 지금까지 클래스 안에서 사용되는 deco decorator를 만들었습니다

2.1 decorator에 인수를 넘겨서 decorator의 동작을 변경하기

  • 간단하게 활용을 해보겠습니다.
  • ts파일 확인 명령어는 아래 명령어로 통일입니다!
ts-node deco.ts
function deco(value: string) {
  console.log("데커레이터가 평가됨.");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log(value);
  };
}

class TestClass {
  @deco("Hi")
  test() {
    console.log("인수에게 먼저 인사한 함수가 호출됨");
  }
}

const t = new TestClass();
t.test();
// 출력
데커레이터가 평가됨.
Hi
인수에게 먼저 인사한 함수가 호출됨
  • decorator에 인수를 넣어서 decorator의 동작을 변경하는것을 실헙해봤습니다.

3. decorator 합성

  • decorator의 합성은 간단합니다.
@a
@b
method()
  • 이런 decorator의 연속 사용은 수학적으로 a(b())와 동일합니다.
  • 이때 평가방식과 호출방식도 동일하게 진행됩니다. (평가는 위에서부터, 호출은 아래서부터)
  • 따라해봅시다
function first() {
  console.log("1. first decorator factory가 평가됨.");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("4. first decorator가 호출됨.");
  };
}

function second() {
  console.log("2. second decorator factory가 평가됨.");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("3. second decorator가 호출됨.");
  };
}

class ExampleClass {
  @first()
  @second()
  method() {
    console.log("5. 모든 decorator를 통과한 후 메서드가 호출됨.");
  }
}

const t = new ExampleClass();
t.method();
// 출력
1. first decorator factory가 평가됨.
2. second decorator factory가 평가됨.
3. second decorator가 호출됨.
4. first decorator가 호출됨.
5. 모든 decorator를 통과한 후 메서드가 호출됨.

4. 타입스크립트에서 지원하는 5가지 decorator

4.1 클래스 decorator

  • 클래스 decorator는 이름 그대로 클래스 바로 앞에 선언됨.
  • 클래스 decorator는 생성자에 적용되어 클래스 정의를 읽거나 수정할 수 있음.
  • 단, 선언 파일과 선언 클래스 내에서는 사용할 수 없음.
  • 클래스 decorator는 생성자를 리턴하는 함수여야 함.
function userClassDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    profileImageURL = "http://www.example.com";
  };
}

@userClassDecorator
class User {
  type = "User";
  name: string;

  constructor(t: string) {
    this.name = t;
  }
}

const user = new User("Decorator");
console.log(user);
// 출력
User {
  type: 'User',
  name: 'Decorator',
  profileImageURL: 'http://www.example.com'
}
  • User 클래스에 선언되지 않았던 새로운 속성이 추가되었습니다.
  • 단 클래스의 타입이 변경되는것은 아님. 타입시스템은 profileImageURL을 인식하지 못하기 때문에 user.profileImageURL을 사용할 수 없음.

4.2 메서드 decorator

  • 메서드 decorator는 메서드 바로 앞에 선언됨.
  • 메서드의 속성 설명자(property descriptor)에 적용되고 메서드의 정의를 읽거나 수정할 수 있음.
  • 선언파일, 오버로드 메서드, 선언 클래스에 사용 못함.
  • decorator를 만드는 함수는 3가지 인자가 필수적으로 들어감
    • targer : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
    • propertyKey: 멤버의 이름
    • descriptor: 멤버의 속성 설명자. PropertyDescriptor 타입을 가짐
  • 만약에 method decorator가 값을 반환한다면, 해당 method의 descriptor임.
  • 함수를 실행할 때 에러가 발생하면 에러를 핸들링하는 예시로 확인해보겠습니다!
function ErrorHandler() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("target :", target);
    console.log("propertyKey :", propertyKey);
    console.log("descriptor :", descriptor);

    const method = descriptor.value;

    descriptor.value = function () {
      try {
        method();
      } catch (error) {
        console.log(error);
      }
    };
  };
}

class User {
  @ErrorHandler()
  error() {
    throw new Error("method decorator에서 에러를 핸들링하였습니다.");
  }
}

const user = new User();
user.error();
// 출력
target : {}
propertyKey : error
descriptor : {
  value: [Function: error],
  writable: true,
  enumerable: false,
  configurable: true
}
Error: method decorator에서 에러를 핸들링하였습니다.

4.2.1 PropertyDescriptor

  • 메서드 decorator가 가져야 하는 3개의 인수
interface PropertyDescriptor {
  configurable?: boolean;  // 속성의 정의를 수정할 수 있는지 여부
  enumerable?: boolean;    // 열거형인지 여부
  value?: any;             // 속성 값
  writable?: boolean;      // 수정 가능 여부
  get?(): any;             // getter
  set?(v: any): void;      // setter
}

4.3 접근자 decorator

  • 접근자 decorator는 접근자(accessor)바로 앞에 선언
  • 접근자의 정의를 읽거나 수정할 수 있음.
  • 선언파일과 선언클래스에는 사용할 수 없음.
  • 접근자 decorator가 반환하는 값이 있다면 해당 멤버의 속성 설명자가 됨.
  • 특정 멤버가 열거가 가능한지 결정하는 예시로 확인해보겠습니다!
function Enumerable(enumerable: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = enumerable;
  };
}

class User {
  constructor(private name: string) {}

  @Enumerable(true)
  get getName() {
    return this.name;
  }

  @Enumerable(false)
  set setName(name: string) {
    this.name = name;
  }
}

const user = new User("test");
for (let key in user) {
  console.log(`${key}: ${user[key]}`);
}
  • getName은 출력이 되지만 setName은 출력이 안된것을 확인할 수 있음.
// 출력
name: test
getName: test

4.4 속성 decorator

  • 속성 decorator는 클래스의 속성 바로 앞에 선언됨.
  • 선언파일과 선언클래스에는 사용할 수 없음.
  • 속성 decorator는 2개의 인자를 가짐
    • target : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
    • propertyKey : 멤버의 이름
  • 속성 설명자가 없고 반환값도 무시됨.
  • 속성을 읽을때 getter를 호출하여 속성을 바꿔서 출력되는 decorator로 설명하겠습니다!
function format(formatString: string) {
  return function (target: any, propertyKey: string): any {
    let value = target[propertyKey];

    function getter() {
      return `${formatString} ${value}`;
    }

    function setter(newVal: string) {
      value = newVal;
    }

    return {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    };
  };
}

class Greeter {
  @format("Hello")
  greeting: string;
}

const t = new Greeter();
t.greeting = "World";
console.log(t.greeting);
// 출력
Hello World

4.5 매개변수 decorator

  • 매개변수 decorator는 생성자나 메서드의 매개변수에 선언되어 사용됨.
  • 선언파일과 선언클래스에는 사용할 수 없음.
  • 매개변수 decorator는 3개의 인자를 가짐, 단 반환값은 무시됨.
    • target : 정적 멤버가 속한 클래스의 생성자 함수이거나 인스턴스 멤버에 대한 클래스의 프로토타입
    • propertyKey : 멤버의 이름
    • parameterIndex : 매개변수가 함수에서 몇 번째 위치에 선언되었는지를 나타내는 인덱스
  • 매개변수가 제대로 전달되었는지 검사하는 decorator로 설명하겠습니다! (일반적으로 함수 decorator와 같이 사용됨)
  • 해당 패키지를 설치해주시고 코드를 복사하셔서 ts-node deco.ts를 입력해주세요
npm i @nestjs/common
import { BadRequestException } from "@nestjs/common";

function MinLength(min: number) {
  //파라미터의 최소값을 검사하는 파라미터 데코레이터
  return function (target: any, propertyKey: string, parameterIndex: number) {
    target.validators = {
      // validators 속성에 유효성을 검사하는 함수 할당
      minLength: function (args: string[]) {
        return args[parameterIndex].length >= min;
      },
    };
  };
}
//유효성 검사 :  parameterIndex에 위치한 인자의 길이가 최소값보다 같거나 큰지 검사
function Validate(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args) {
    Object.keys(target.validators).forEach((key) => {
      if (!target.validators[key](args)) {
        throw new BadRequestException();
      }
    });
    method.apply(this, args);
  };
}

class User {
  private password: string;

  @Validate
  setPassword(@MinLength(8) password: string) {
    this.password = password;
  }
}

const t = new User();
t.setPassword("password"); // 길이가 8이상이라 문제없음
console.log("이 선을 넘으면 통과!");
t.setPassword("pass"); //길이가 8보다 작기 때문에 BadRequestException 발생
// 출력
이 선은 넘으면 통과!
BadRequestException: Bad Request

4.6 요약

데코레이터[@] 역할 호출시 전달되는 인자 선언 불가능한 위치
클래스 데코레이터 클래스의 정의를 읽거나 수정 (constructor) d.ts 파일, declare 클래스
메서드 데코레이터 메서드의 정의를 읽거나 수정 (target, propertyKey, propertyDescriptor) d.ts 파일, declare 클래스, 오버로드 메서드
접근자 데코레이터 접근자의 정의를 읽거나 수정 (target, propertyKey, propertyDescriptor) d.ts 파일, declare 클래스
속성 데코레이터 속성의 정의를 읽음 (target, propertyKey) d.ts 파일, declare 클래스
매개변수 데코레이터 매개변수의 정의를 읽음 (target, proeprtyKey, parameterIndex) d.ts 파일, declare 클래스

 

결론

  • 몸이 안좋아서 클래스 정리를 미루고 가볍게 시작했는데 예기치 못한 에러를 만나서 3시간을 내리 날려버렸다....

2023.10.22 - [에러] - [23.10.22] .ts를 js로 컴파일하는것과 ts-node

 

[23.10.22] .ts를 js로 컴파일하는것과 ts-node

에러 나는 이 둘이 같은것인줄 알았다. 하지만 너무 다르다. 단순히 동작이 1가지에서 2가지로 바뀌는게 아니고 이 둘은 완전히 다르다. 본론 nestjs코드를 확인하기 위해서 간단하게 프로젝트를

paikpaik.tistory.com

  • decorator는 그냥 class-validator를 많이 쓰지만 이제부터 커스텀해서 코드를 리팩토링도 해야겠다.