yceffort

자바스크립트 데코레이터

Published on May 19, 2020

데코레이터

0. 설명자

데코레이터에 대해 시작하기 전에, 설명자(Descriptor)에 대해 알아보자.

설명자란, 객체의 프로퍼티가 쓰기가 가능한지, 그리고 열거가 가능한지 여부를 나타낸다. 그리고 설명자를 구현하기 위해서는, Object.getOwnPropertyDescriptor(obj, propName) 를 사용해야 한다. 아래의 예를 살펴보자.

const hello = {
  get hi() {
    return 'hello'
  },
  number: 42
}

console.log(Object.getOwnPropertyDescriptor(hello, 'hi'))
console.log(Object.getOwnPropertyDescriptor(hello, 'number'))

Object.defineProperties(hello, {
  hell: {
    value: 1, 
    enumerable: false,
    configurable: false,
    writable: false
  }
})
console.log(Object.getOwnPropertyDescriptor(hello, 'hell'))
{
  get: [Function: get hi],
  set: undefined,
  enumerable: true,
  configurable: true
},
{ value: 42, writable: true, enumerable: true, configurable: true },
{ value: 1, writable: false, enumerable: false, configurable: false }
  • writable은 객체의 프로퍼티가 쓰기 가능한지의 여부다.
hello.number = 41
console.log(hello.number) //41 로 바꼈다.

hello.hell = 10
console.log(hello.hell) // 1이 리턴되며, 바뀌지가 않는다.
  • enumberable은 객체의 프로퍼티가 열거 가능한지의 여부이며, false라면 Object.keys, Object.values, Object.entries등에서도 해당 프로퍼티를 볼 수 없다.
console.log(Object.keys(hello)) // [ 'hi', 'number' ] 가 뜨며, hell은 안보인다 ㅠㅠ
  • configurable은 해당 프로퍼티가 defineProperty를 통해 설정될 수 있는지 여부이며, false라면 defineProperty로 해당 객체를 설정할 수가 없다.
// 다시 define 해보자
Object.defineProperties(hello, {
  hell: {
    value: true, 
    enumerable: true,
    configurable: true,
    writable: true
  }
}) // TypeError: Cannot redefine property: hell
  • 위에서 본 것처럼 gettersetter도 있는데, 이는 주로 동적으로 계산한 값을 반환하는 프로퍼티에 접근하거나, 메소드 호출을 하지 않고도 내부 변수에 접근해야 하는 경우 등에 사용한다.

1. 데코레이터

데코레이터는 클래스의 프로퍼티 / 메소드 / 클래스 자체를 수정하는데 사용되는 자바스크립트 함수다. @xxx로 작성 될 수 있으며 수정할 프로퍼티 / 메소드 / 클래스 윗줄에 추가해주면 된다. 이는 적용된 메소드가 호출되거나, 인스턴스가 만들어지는 것과 같은 런타임에 실행된다.

클래스 데코레이터

클래스 위에 선언되어, 클래스 자체를 수정하는 예제.

// 클래스의 constructor를 덮어쓴다.
function setName(name: string) {
  return <T extends { new(...args: any[]): {} }>(constructor: T) => {
    return class extends constructor {
        name = name
    }  
  }  
}


@setName('trump')
class President {
  
  name: string

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

  sayHello() {
    console.log(`hello, ${this.name}`)
  }
}

const t = new President('obama')
console.log(t.sayHello()) // hello, trump

메소드 데코레이터

아래 예제에서는 메소드에 데코레이터가 쓰였으며, 메소드에 logger를 달거나, readOnly등의 속성으로 writable을 손쉽게 막을 수 있다.

function readOnly(isReadOnly: boolean) {
  return function(target: Person, propName: string, description: PropertyDescriptor) {
    description.writable = isReadOnly;
  }
}

const logger = (message: string) => (target: Person, propName: string, description: PropertyDescriptor) => {
    const value = description.value

    description.value = function (...args: any) {
      console.log('LOG >>>', message)
      return value.apply(this, args)
    }
  }

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

  @logger('Say hello name')
  @readOnly(false)
  sayHello() {
    return `hello, ${this.name}`
  }
}

const trump = new Person('trump')

console.log(trump.sayHello())
// LOG >>> Say hello name
/// hello, trump

trump.sayHello = () => 'hi xxx'
// Cannot assign to read only property 'sayHello' of object '#<Person>'

접근자 데코레이터 (Access Decortaor)

접근자 데코레이터는, 접근자를 선언하기 바로 직전에 선언된다. 이를 이용해 접근자의 정의를 관찰, 수정, 교체 하는 등에도 사용할 수 있다.

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

class Person {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }

    @configurable(false)
    get hi() { return this.name; }
}

이 밖에도 프로퍼티 데코레이터, 매개변수 데코레이터 등이 있다. 이 두가지 케이스는, reflect-metadata를 사용해야 하고, 아직 공식적으로 ECMA에 채택되지도 않았다.