Medium - What Is a Decorator in JavaScript?
in Trend
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
에 의해서 호출됩니다. 따라서 함수 내부에 있는 getWriter
의 this
는 fakeMedium
내부에 있는 값을 바라보게 됩니다.
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
로그가 두번 찍히게 됩니다. 왜 그럴까요?
- 먼저
fakeMedium.getWriter()
를 호출하면 내부 getter가 실행되며 커스텀된get
메소드를 실행합니다. get
메소드 내부에서getWriter
는 새로운medium
-this.getWriter.bind(medium)
에 바인딩됩니다. 여기서this
는fakeMedium
을 참조합니다. 따라서fakeMedium.getWriter.bind(medium)
과 같아지죠. 이것이 get이 두번 불리는 이유입니다.- 그러나 함수가 바운드 되기 전에
isDefining
이 참으로 설정되어 있으면 아래에 있는 if 구문이 실행되지 않을 것입니다.
그러나 이렇게 하면 매번 새로운 Medium
인스턴스를 만들 때 마다 이 프로세스를 반복해야 합니다. 이것을 좀 더 우아하게 수행할 수는 없을까요?
With a Decorator
모든 함수는 데코레이터가 될 수 있습니다. 기본적으로 데코레이터는 클래스와 클래스 내부 메소드 둘 다 쓰입니다. 데코레이터는 target, value, descriptor 3개의 매개변수를 필요로 합니다.
function decorator(target, value, descriptor) {}
- 타켓은 클래스나 클래스 프로퍼티를 참조합니다.
- 클래스의 값은 undefined가 되고 메소드의 이름은 undefined입니다.
- 디스크립터는 객체에서 정의가능한 객체들을 가지고 있는 객체입니다. 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 등을 사용해줘야 하지만 매번 새로운 인스턴스를 만들 때마다 번거롭게 수행해야 함.
- 데코레이터를 쓰면 매우 깔끔해진다.