본문 바로가기

BackEnd

JS OOP 시리즈 3 : 자바스크립트 데코레이터 이해하기

반응형

해당 게시물은 여기서도 볼 수 있다.

OOP 시리즈 3 : 자바스크립트 데코레이터 이해하기 (velog.io)

Reference

이 게시물은 https://ui.toast.com/weekly-pick/ko_20200102를 요약 정리한 내용이다.
또한 아래 게시물을 많이 참고하였다.
https://ko.javascript.info/property-descriptors
https://ko.javascript.info/property-accessors

프록시를 살펴본 이유

지금까지 프록시를 통해 pojo(plain-old-javascript-object)에 마법처럼 기능을 추가할 수 있다는 것을 보았다.

데코레이터는 프록시를 더욱 쉽게 구현할 수 있게 해준다.

데코레이터는 데코레이터 함수, 메소드의 약자이다.
새 함수를 반환하여 전달된 함수, 메소드의 동작을 수정하는 함수다.

즉, 프록시와 같은 기능을 할 수 있다

자바스크립트 데코레이터와 속성 설명자.

자바스크립트 객체에는 속성이 존재하며, 각 속성은 값(함수 포함)을 갖는다.
각 속성은 추가로 각 속성들이 어떻게 작동할지에 대한 정의를 갖고 있다. 이를 descriptor라 한다.

const oatmeal = {
  viscosity: 20,
  flavor: 'Brown Sugar Cinnamon',
};

console.log(Object.getOwnPropertyDescriptor(oatmeal, 'viscosity'));
/*
{
  configurable: true,
  enumerable: true,
  value: 20,
  writable: true
}
*/

객체의 프로퍼티는 두 종류로 나뉜다.

하나는 데이터 프로퍼티다. getter, setter를 제외한 모든 프로퍼티는 데이터 프로퍼티다.
데이터 프로퍼티는 값(value) 과 함께 플래그(flag)라 불리는 특별한 속성 세 가지를 갖는다.

  • writable – true : 값을 수정할 수 있다. / false : 읽기만 가능하다.
  • enumerable – true : 반복문을 사용해 나열할 수 있습니다. / false : 반복문을 사용해 나열할 수 없다.
  • configurable – true이면 프로퍼티 삭제나 플래그 수정이 가능. false면 프로퍼티 삭제/ 플래그 수정 불가.

다른 하나는 접근자 프로퍼티다.

접근자 프로퍼티의 본질은 함수이다. (값마저도 자신을 메서드로 만든다.)
getter, setter는 값을 획득하고 설정하는 역할을 담당한다.
외부 코드에서는 일반적인 프로퍼티로 보인다.
접근자 프로퍼티엔 설명자 value와 writable가 없는 대신에 get과 set이라는 함수가 있다.
따라서 접근자 프로퍼티는 다음과 같은 설명자를 갖는다.

  • get – 인수가 없는 함수로, 프로퍼티를 읽을 때 동작함
  • set – 인수가 하나인 함수로, 프로퍼티에 값을 쓸 때 호출됨
  • enumerable – 데이터 프로퍼티와 동일함
  • configurable – 데이터 프로퍼티와 동일함

configurable 플래그를 false로 설정하면 돌이킬 방법이 없음.
configurable: false는 플래그 값 변경이나 프로퍼티 삭제를 막기 위해 만들어졌지, 프로퍼티 값 변경을 막기 위해 만들어진 게 아니다.
value 속성은 자신을 자신이 속한 객체의 메서드로 만든다.

객체의 프로퍼티와 디스크립터를 직접 수정하기

Object.getOwnPropertyDescriptor 및 Object.defineProperty를 통해 객체의 구조, 행위를 직접 수정할 수 있다.

/** 읽기 전용 만들기.*/
Object.defineProperty(oatmeal, 'viscosity', {
  writable: false,
  value: 20,
});

// `oatmeal.viscosity`를 다른 값으로 설정하면 에러 없이 실패한다.
oatmeal.viscosity = 30;
console.log(oatmeal.viscosity);
// => 20

데코레이터 함수decorate를 만들 수도 있다.

function decorate(obj, property, callback) {
  var descriptor = Object.getOwnPropertyDescriptor(obj, property);
  Object.defineProperty(obj, property, callback(descriptor));
}

decorate(oatmeal, 'viscosity', function(desc) {
  desc.configurable = false;
  desc.writable = false;
  desc.value = 20;
  return desc;
});

참고 : https://ko.javascript.info/proxy 페이지의 번역이 잘못되어 있다.

  • Object.getOwnPropertyNames(obj) : 심볼이 아닌 키만 반환
  • Object.getOwnPropertySymbols(obj) : 심볼인 키만 반환
  • Object.keys/values() : 논심볼, enumerable한 키와 값.
  • for..in loops : 논심볼, enumerable한 키, 프로토타입 키 순회.

데코레이터 사용하기

자바스크립트 데코레이터가 위의 직접 수정, 함수형과 다른 점은 클래스에 관심을 둔다는 점이다.

class Porridge {
  constructor(viscosity = 10) {
    this.viscosity = viscosity;
  }

  stir() {
    if (this.viscosity > 15) {
      console.log('This is pretty thick stuff.');
    } else {
      console.log('Spoon goes round and round.');
    }
  }
}

class Oatmeal extends Porridge {
  viscosity = 20;

  constructor(flavor) {
    super();
    this.flavor = flavor;
  }
}

참고 : 클래스 필드

데코레이터 작성하는 법

JS 데코레이터 함수에는 세 가지 인자가 전달된다.

  1. target은 현재 인스턴스 객체의 클래스이다.
  2. key는 데코레이터를 적용할 속성 이름이다(문자열).
  3. descriptor는 해당 속성의 설명자 객체이다.

리턴값은 어디에 쓰이느냐에 따라 다르다만...
리턴값이 없으면 적절한 부수효과를 일으키고 끝난다.
(직접 파라미터를 수정하는 방식을 사용할 수 있음.)

  • 클래스 데코레이터 : 클래스(생성자)를 반환하면 해당 클래스를 대체함.
  • 메소드(접근자 프로퍼티 포함) 데코레이터 : 메서드 속성설명자
  • 프로퍼티 데코레이터 : 속성설명자 (주의 : 타입스크립트는 현재 디스크립터 인자를 사용할 수 없음, 리턴값도 무시됨.)
  • 파라미터 데코레이터 : 링크에 따르면 js 사양에는 아직 존재하지 않음. 하지만 타입스크립트에는 존재함. 타입스크립트 시간에 알아보자.

속성을 읽기 전용으로 만드는 프로퍼티 데코레이터

function readOnly(target, key, descriptor) {
  return {
    ...descriptor,
    writable: false,
  };
}

// 이렇게 쓴다.
class Oatmeal extends Porridge {
  @readOnly viscosity = 20;
  // (@readOnly를 속성 바로 윗 줄에 적어도 된다.)

  constructor(flavor) {
    super();
    this.flavor = flavor;
  }
}

API 오류 처리 메서드 데코레이터

// 메서드 데코레이터
function apiRequest(target, key, descriptor) {
  const apiAction = async function(...args) {
       // value에 메서드가 들어가 있다.
    const original = descriptor.value || descriptor.initializer.call(this);
    /**  해당 데코레이터 사용 클래스에서 해당 함수를 구현해야 함.*/
    this.setNetworkStatus('loading');

    try {
      const result = await original(...args);
      return result;
    } catch (e) {
    /**  해당 데코레이터 사용 클래스에서 해당 함수를 구현해야 함.*/
      this.setApiError(e);
    } finally {
      /**  해당 데코레이터 사용 클래스에서 해당 함수를 구현해야 함.*/
      this.setNetworkStatus('idle');
    }
  };

  return {
    ...descriptor,
    value: apiAction,
    initializer: undefined, // 이건 아래에서 설명
  };  
}

// 아래와 같이 사용한다.
class WidgetStore {
  @apiRequest
  async getWidget(id) {
    const { widget } = await api.getWidget(id);
    this.addWidget(widget);
    return widget;
  }
}

이니셜라이저는 뭘까? 화살표 함수가 들어오면 사용된다.

class Example {
  @myDecorator
  someMethod() {
    // 이 경우, 메서드는 descriptor.value를 참조할 것이다.
  }

  @myDecorator
  boundMethod = () => {
    // 여기서 descriptor.initializer는 함수 자신이 될것이다. 이 함수가 실행되면 `boundMethod`함수를 반환할 것이고 스코프가 올바르게 만들어져서 `this`는 실행 당시의의 Example의 인스턴스 일 것이다.
  };
}

클래스 데코레이터

해당 데코레이터는 뒤에서 많이 다루도록 한다.

function logWhenInit(text) {
  return function(target) {
    console.log(text);
  };
}


@logWhenInit('hello world');
class InitLoggerDecoratorTest {
  constructor() {}
}

// 처음 실행할 때 클래스 로딩시 단 한번만 로그가 찍힌다.

다음 시간에는...

타입스크립트에서 지원하는 데코레이터의 추가 기능에 대해 알아본다.

반응형