Medium - What Is a Decorator in JavaScript?

원문 - Moon

Trend 파악을 위한 Medium 기고문 포스팅 - 자바스크립트에서 데코레이터는 무엇일까요? 데코레이터를 사용해서 여러분의 코드를 깔끔하게 정리하세요.

Photo by Kira auf der Heide on Unsplash

자바스크립트로 클래스를 작성할 때 여러분은 클레스에 있는 메소드들에 기능을 더 추가하고 싶을 때가 있을겁니다. 그러나 그러면 너무 지저분해 보일 때가 있죠.

이런 프로세스를 더욱 우아하게 처리할 수 있는 방법이 있을까요? 이 포스트에서는 아주 유용한 기능은 데코레이터에 대해서 얘기할 것입니다.

Before Reading

최신 ECMA-262 (자바스크립트)에 이 기능은 포함되어 있지 않습니다. 따라서 바벨을 프로젝트에서 사용하셔야 합니다. 데코레이터 proposal은 현재 stage2에 있으며 GitHub에서 확인하실 수 있습니다.

제가 첨부한 예제는 JSFiddle, Babel + JSX 설정으로 작성되었습니다. 만약 여러분의 프로젝트에서 실행하고 싶다면 바벨을 설정하셔야 합니다.

Without a Decorator

class Medium {
  constructor(writer) {
    this.writer = writer;
  }

  getWriter() {
    return this.writer;
  }
}

미디엄 클래스는 생성자에서 작성자의 이름을 입력받고 getWriter()함수를 통해 작성자의 이름을 리턴합니다. 그럼 Medium 타입의 프로퍼티를 만들어 봅시다.

const medium = new Medium('Jane');

const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
}

medium은 Medium생성자를 사용해서 만들었으며 fakeMedium과 달리 객체 러터럴입니다. 그래도 둘다 medium이라는 프로퍼티를 가지고 있죠. 그러면 이제 getWriter의 결과를 각각 비교해봅시다.

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Fake Jane

왜 값이 다르게 나오는 걸까요? 왜냐면 자바스크립트의 일반 함수인 this는 함수가 실제로 호출된 객체에 연결되기 때문입니다. medium.getWriter()medium 객체에 의해서 호출되지만 fakeMedium.getWriter()fakeMedium에 의해서 호출됩니다. 따라서 함수 내부에 있는 getWriterthisfakeMedium내부에 있는 값을 바라보게 됩니다.

medium.getWriter을 호출해서 같은 ㄹ값을 얻기 위해서는 Object.defineProperty를 사용해야 합니다. Object.definedProperty가 하는 것은 객체의 새로운 프로퍼티를 정의하거나 이미 객체에 있는 프로퍼티를 수정해서 객체 자체를 리턴하는 것입니다.

const fakeMedium = { ... };
let isDefining;
let fn = fakeMedium.getWriter;
Object.defineProperty( fakeMedium, 'getWriter', {
  get() {
    console.log('Access to getWriter');
    if ( isDefining ) {
      return fn;
    }
    isDefining = true;
    const boundFn = this.getWriter.bind(medium);
    isDefining = false;

    return boundFn;
  }
});

fakeMedium.getWriter가 호출되면 Access to getWriter 로그가 두번 찍히게 됩니다. 왜 그럴까요?

  1. 먼저 fakeMedium.getWriter()를 호출하면 내부 getter가 실행되며 커스텀된 get 메소드를 실행합니다.
  2. get메소드 내부에서 getWriter는 새로운 medium - this.getWriter.bind(medium)에 바인딩됩니다. 여기서 thisfakeMedium을 참조합니다. 따라서 fakeMedium.getWriter.bind(medium)과 같아지죠. 이것이 get이 두번 불리는 이유입니다.
  3. 그러나 함수가 바운드 되기 전에 isDefining이 참으로 설정되어 있으면 아래에 있는 if 구문이 실행되지 않을 것입니다.

그러나 이렇게 하면 매번 새로운 Medium인스턴스를 만들 때 마다 이 프로세스를 반복해야 합니다. 이것을 좀 더 우아하게 수행할 수는 없을까요?

With a Decorator

모든 함수는 데코레이터가 될 수 있습니다. 기본적으로 데코레이터는 클래스와 클래스 내부 메소드 둘 다 쓰입니다. 데코레이터는 target, value, descriptor 3개의 매개변수를 필요로 합니다.

function decorator(target, value, descriptor) {}
  1. 타켓은 클래스나 클래스 프로퍼티를 참조합니다.
  2. 클래스의 값은 undefined가 되고 메소드의 이름은 undefined입니다.
  3. 디스크립터는 객체에서 정의가능한 객체들을 가지고 있는 객체입니다. configurable, writable, enumerable, value, 그 값들은 클래스를 위한 undefined 입니다.
function autobind(target, value, descriptor) {}
class Medium {
  ...
  @autobind
  getWriter() {
    return this.writer;
  }
}

데코레이터는 @심볼과 함께 사용되며 함수 이름에 앳을 쓰면 여러분은 데코레이터를 사용하게 되는 것입니다. 그러면 위에 설명드린대로 3개의 매개변수를 받게 되죠.

function autobind(target, value, descriptor) {
  const fn = descriptor.value;

  return {
    configurable: true,
    get() {
      return fn.bind(this);
    }
  }
}

descriptor.value는 여러분이 데코레이터 함수에 넣은 함수의 이름입니다. 여기서는 getWriter가 되겠죠.

autobind는 새로운 객체로서 값을 리턴하는 것을 기억하세요. 그다음 getWriter는 그 값을 환경에 적용합니다.

데코레이터를 사용해서 좋은 것은 항상 재사용가능하기 때문입니다/ 앞에서 함수 데코레이터를 정의했다면 바로 @autobind를 함수에 사용해서 작성할 수 있습니다.

다음은 훨씬 쉽게 read-only 클래스 멤버 프로퍼티를 만드는 예제입니다.

function readonly(target, value, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
class Medium {
  @readonly
  signUpdate = '2019-04-23';
}
const medium = new Medium();
medium.signUpdate; // 2019-04-23
medium.signUpdate = '1999-11-11';
medium.signUpdate; // 2019-04-23
// 값이 변경되지 않았습니다!

이번에는 디스크립터의 프로퍼티인 writable을 그냥 false로 설정한 것이 전부입니다. 너무 쉽지 않나요? 그렇죠?

Full Code Comparison

다음은 전체 코드 비교입니다.

class Medium {
  constructor(writer) {
    this.writer = writer;
  }

  getWriter() {
    console.log( this.writer );
  }
}

const medium = new Medium('Jane');
const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
};

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Fake Jane;


/*같은 값에 대해서 자동 바인딩을 해주는 JOB*/
let isDefining;
let fn = fakeMedium.getWriter;
Object.defineProperty(fakeMedium, 'getWriter', {
  get() {
    if (isDefining) {
      return fn;
    }
    isDefining = true;
    const boundFn = this.getWriter.bind(medium);
    isDefining = false;
  }
});

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Jane

Without decorator

function autobind(target, value, descriptor) {
  const fn = descriptor.value;

  return {
    configurable: true,
    get() {
      return fn.bind(this);
    }
  }
}

class Medium {
  constructor(writer) {
    this.writer = writer;
  }

  @autobind
  getWriter() {
    console.log( this.writer );
  }
}

const medium = new Medium('Jane');
const fakeMedium = {
  writer: 'Fake Jane',
  getWriter: medium.getWriter,
};

medium.getWriter(); // Jane
fakeMedium.getWriter(); // Jane

Conclusion

데코레이터는 아주 아주 아주 아주 유용하고 강력하고 놀랍습니다. 솔직하게 왜 이 기능을 안쓰시는지 모르겠네요. 그렇지만 데코레이터가 여전히 stage2에 있고 제가 작성한 것은 Babel의 스타일과 더 비슷합니다. 그렇기 때문에 많은 것이 달라질 수 있고 여러분이 실제로 할 수 있는 것은 다를 수도 있습니다.

그렇기 때문에 적절하게 바벨 설정을 여러분의 프로젝트에 한다음에 사용하는 것을 추천드립니다.

Summary

  • 자바스크립트에서 this가 호출된 객체에 바인딩 되기 때문에 같은 객체의 메소드라도 다르게 동작함
  • 해당 건을 해결하기 위해서는 Object.defineProperty 등을 사용해줘야 하지만 매번 새로운 인스턴스를 만들 때마다 번거롭게 수행해야 함.
  • 데코레이터를 쓰면 매우 깔끔해진다.

© 2019. All rights reserved.

Powered by Hydejack v8.1.1