[JavaScript] 데코레이터 (Decorator)
2024.02.04- -
1. 데코레이터
1-1. Decorator Pattern (데코레이터 패턴)
데코레이터는 무엇일까요? 파이썬을 해 보신 분들은 아시겠지만 파이썬에서 데코레이터는 고차 함수를 호출하기 위한 매우 간단한 구문을 제공합니다. 파이썬 데코레이터는 다른 함수를 받아 명시적으로 수정하지 않고도 후자의 함수의 동작을 확장하는 함수입니다. 파이썬에서 가장 간단한 데코레이터는 다음과 같이 보일 수 있습니다:
저 맨 위에 붙어 있는 것(`@mydecorator`)이 데코레이터이며, ES2016(ES7)에서도 크게 달라지지 않습니다.
'@'는 Parser(파서)에게 데코레이터를 사용 중임을 알리게 위해 표시하는 것이며, mydecorator는 해당 이름으로 함수를 참조합니다. 데코레이터는 인자(데코레이션되는 함수)를 받아 기능이 추가된 동일한 함수를 반환합니다.
데코레이터는 추가 기능으로 무언갈 감싸고 싶을 때 유용합니다. 여기에는 메모 작성, 액세스 제어 및 인증 적용, 계측 및 타이밍 기능, 로깅, 속도 제한 등이 포함될 수 있습니다.
1-2. ES5와 ES6에서의 데코레이터
ES5에서는 명령형 데코레이터를 (순수 함수로) 구현하는 것이 아주 간단했습니다. ES6에서는 클래스가 extenstion을 지원하지만, 하나의 기능을 공유해야 하는 클래스가 여러 개 있는 경우 더 나은 배포 방법이 필요하게 됩니다.
Yehuda(데코레이터 만든사람)가 제안한 데코레이터의 방식은 선언적 구문(declarative syntax)을 유지하면서 디자인 시점에 자바스크립트 클래스(class), 속성(property), 객체 리터럴(object literal)에 주석을 달고 수정할 수 있도록 하려는 것입니다.
ES2016 데코레이터가 실제로 어떻게 작동하는지 살펴보겠습니다.
1-3. ES2016 데코레이터
ES2016 데코레이터는 함수를 반환하고 target, name, property 설명자를 인자로 받을 수 있는 표현식입니다. 데코레이터 앞에 '@' 문자를 붙이고 데코레이션하려는 항목의 맨 위에 배치하여 적용합니다. 데코레이너는 클래스 또는 속성에 대해 정의할 수 있습니다.
class Cat {
meow() { return `${this.name} says Meow`; }
}
이 클래스를 evaluate 하면, 대충 아래과 같이 `Cat.prototype`에 meow 함수를 설치하게 됩니다:
속성이나 메서드 이름을 수정할 수 없도록 하기 위해 마킹하고 싶다고 가정해 봅시다. 데코레이터는 프로퍼티를 정의하는 구문 앞에 옵니다. 따라서 다음과 같이 `@readonly` 데코레이터를 정의할 수 있습니다:
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
그리고 이를 확인하기 위해 다음과 같이 meow 속성에 해당 데코레이터를 적용해봅시다:
class Cat {
@readonly
meow() { return `${this.name} says Meow`; }
}
데코레이터는 평가될 표현식일 뿐이며 함수를 반환해야 합니다. 이것이 바로 `@readonly`와 `@something(parameter)`와 같이 두 가지 모두 작동할 수 있는 이유입니다.
이제 `Cat.prototype`에 descriptor를 설치하기 전에 엔진은 먼저 데코레이터를 호출합니다:
let descriptor = {
value: specifiedFunction,
enumerable: false,
configurable: true,
writable: true
};
// 데코레이터는 Object.defineProperty와 동일한 시그니처를 갖고,
// 관련된 defineProperty가 실제로 발생하기 전에 끼어들 기회를 얻는다.
descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
Object.defineProperty(Cat.prototype, 'meow', descriptor);
이제 meow는 읽기 전용이 되었습니다. 다음 예시에서 그것을 확인할 수 있습니다:
var garfield = new Cat();
garfield.meow = function() {
console.log('I want lasagne!');
}
// Exception: Attempt to assign to readonly property
(속성이 아닌) 데코레이션 클래스에 대해서는 잠시 후에 살펴볼 예정이지만, 잠시 라이브러리에 대해 이야기해 보겠습니다. 아직 나온지 얼마 되지 않았음에도 불구하고 제이 펠프스의 https://github.com/jayphelps/core-decorators.js 등 2016 데코레이터들의 라이브러리들이 이미 등장하기 시작했습니다.
위의 readonly 속성에 대한 시도와 유사하게, 이 라이브러리에는 `@readonly`에 대한 자체 구현이 포함되어 있으며, import만 하면 됩니다:
import { readonly } from 'core-decorators';
class Meal {
@readonly
entree = 'steak';
}
var dinner = new Meal();
dinner.entree = 'salmon';
// Cannnot assign to read only property 'entree' of [objet Object]
또한 API에 메서드가 변경될 가능성이 있다는 힌트가 필요할 때를 대비해 사용될 수 있는 `@deprecate`와 같은 다른 데코레이터 유틸리티도 해당 라이브러리에 포함되어 있습니다:
deprecation message와 함께 console.warn()을 호출합니다. default 메시지를 재정의하려면 사custom 메시지를 제공하면 됩니다. 추가 reading을 위해 URL과 함께 옵션 해시를 제공할 수도 있습니다.
import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
faceparlmHarder() {}
}
let captainPicard = new Person();
conatinPicard.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions
conatinPicard.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming
captainPicard.facepalmHarder();
// DEPRECATIOM Person#facepalmHard: We stopped facepalming
1-4. 클래스 데코레이팅하기
다음으로 데코레이터 클래스를 살펴봅시다. 이 경우, 제안된 사양에 따라 데코레이터는 target 생성자를 취합니다. 가상의 `MySuperHero` 클래스의 경우 `@superhero` 데코레이션을 사용하여 다음과 같이 간단한 데코레이터를 정의할 수 있습니다:
function superhero(target) {
target.isSuperhero = true;
target.power = 'flight';
}
@superhero
class MySuperHero() {}
console.log(MySuperHero.isSuperhero); // true
이를 더 확장하면 데코레이터 기능을 팩토리로 정의하기 위한 인자를 제공할 수 있습니다:
function superhero(isSuperhero) {
return function(target) {
target.isSuperhero = isSuperhero
}
}
@superhero(true)
class MySuperHeroClass() {}
console.log(MySuperHeroClass.isSuperhero); // true
@superhero(false)
class MySuperHeroClass() {}
console.log(MySuperHero.isSuperhero); // false
ES2016 데코레이터는 property descriptors와 클래스에서 작동합니다. 곧 다루겠지만 property 이름과 target 객체를 자동으로 전달받습니다. descriptor에 액세스하면 데코레이터가 getter를 사용하기 위해 property를 변경하는 등의 작업을 할 수 있도록 하며, property에 처음 액세스할 때 메서드를 현재 인스턴스에 자동으로 바인딩하는 등 번거로울 수 있는 동작을 수행할 수 있습니다.
1-5. ES2016 Decorator와 Mixins
Reg는 모든 대상(클래스 프로토타입 또는 독립형)에 동작을 혼합하는 helper를 제안하고 클래스별 버전을 설명했습니다.
믹스인 함수는 인스턴스 동작을 클래스의 프로토타입에 혼합하여 특정 클래스에 추가적인 기능이나 속성을 동적으로 "믹스인"하는 데 사용됩니다. 예를 들어, 여러 클래스에 공통적으로 사용될 수 있는 메서드나 속성을 정의하고, 이를 필요한 클래스에 적용할 수 있습니다. 이 패턴은 자바스크립트에서 상속의 유연한 대안으로 사용될 수 있으며, 코드의 재사용성을 높이고 중복을 줄이는 데 도움을 줍니다.
function mixin(behaviour, sharedBehaviour = {}) {
const instanceKeys = Reflect.ownKeys(behaviour);
const sharedKeys = Reflect.ownKyes(sharedBehaviour);
const typeTag = Symbol('isa');
function _mixin(clazz) {
for (let property of instanceKeys) {
Object.defineProperty(clazz.prototype, property, { value: behaviour[property] });
}
Object.defineProperty(clazz.prototype, typeTag, { value: true });
return clazz;
}
for (let property of sharedKeys) {
Object.defineProperty(_mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
}
Object.deinfeProperty(_mixin, Symbol.hasInstance, {
value: (i) => !!i[typeTag]
});
return _mixin;
}
- 이 코드는 mixin 함수를 정의하고 있습니다. 이 함수는 2개의 인자를 받는데, behaviour 객체는 인스턴스에 추가될 속성이나 메서드를 담고 있으며, sharedBehaviour 객체는 믹스인 자체에 추가될 속성이나 메서드를 담고 있습니다.
- sharedBehaviour는 mixin 함수에서 사용되며, 믹스인 자체에 메서드나 속성을 추가하는 데 사용됩니다. 이는 여러 클래스나 객체에 공통으로 적용될 수 있는 메서드나 속성을 정의합니다. 즉, 믹스인이 적용될 때마다 각 인스턴스에만 추가되는 속성이나 메서드('behaviour')뿐만 아니라, 믹스인 자체에 공통적으로 적용되는 기능('sharedBehaviour')도 정의할 수 있습니다.
- 변수 선언:
- instanceKeys와 sharedKeys는 각각 behaviour와 sharedBehaviour 객체의 속성 키를 배열로 추출합니다.
- typeTag는 믹스인을 식별하기 위한 고유 심볼입니다.
- _mixin 함수:
- 이 내부 함수는 클래스를 인자로 받아 해당 클래스의 prototype에 behaviour 객체의 속성을 추가합니다. 이를 통해 인스턴스가 생성될 때 해당 속성들이 인스턴스에 포함됩니다.
- typeTag를 클래스의 prototype에 추가하여, 믹스인이 적용된 클래스의 인스턴스를 식별할 수 있게 합니다.
- 공유 속성 추가:
- sharedKeys를 이용하여 _mixin 함수 자체에 sharedBehaviour의 속성을 추가합니다. 이를 통해 믹스인 자체가 이러한 속성을 가질 수 있게 됩니다.
- Instance 검사:
- Symbol.hasInstance를 사용하여, 어떤 객체가 특정 클래스의 인스턴스인지 확인할 때 사용하는 로직을 커스터마이즈합니다. 여기서는 객체가 'typeTag' 심볼 속성을 가지고 있는지를 통해 믹스인의 적용 여부를 판단합니다.
- _mixin 반환:
- 수정된 _mixin 함수를 반환합니다. 이 함수는 클래스에 믹스인을 적용하는 데 사용됩니다.
좋아요. 이제 몇 가지 mixin을 정의하고 이를 사용하여 클래스를 꾸밀 수 있습니다. 간단한 `ComicBookCharacter` 클래스가 있다고 가정해 봅시다:
class ComicBookCharater {
constructor(first, last) {
this.firstNmae = first;
this.lastName = last;
}
realName() {
return this.firstName + ' ' + this.lastName;
}
};
이 캐릭터는 세상에서 가장 지루한 캐릭터일지 모르지만, 우리는 '슈퍼파워'와 '유틸리티 벨트'를 부여하는 동작을 제공하는 몇 가지 mixin을 정의할 수 있습니다. Reg의 믹스인 헬퍼를 사용해 이 작업을 수행해 보겠습니다:
const SuperPowers = mixin({
addPower(name) {
this.powers().push(name);
return this;
},
powers() {
return this._powers_pocessed || (this._powers_pocessed = []);
}
});
const UtilityBelt = mixin({
addToBelt(name){
this.utilities().push(name);
return this;
},
utilities() {
return this._utility_items || (this._utility_items = []);
}
});
- SuperPowers 믹스인은 객체에 초능력을 추가할 수 있는 addPower 메서드와, 해당 객체가 가진 모든 초능력을 관리하는 powers 메서드를 제공합니다.
- addPower(name) 메서드는 인자로 받은 name을 객체의 _powers_possessed 배열에 추가합니다. 객체가 해당 배열을 아직 가지고 있지 않으면, 새로운 배열을 생성합니다. 메서드는 this를 반환하여 메서드 체이닝을 지원합니다.
- powers() 메서드는 객체의 _powers_possessed 배열을 반환합니다. 이 배열은 객체가 가진 모든 초능력을 저장합니다. 배열이 초기화되지 않았으면, 새로운 배열을 생성하여 반환합니다.
- UtilityBelt 믹스인은 객체에 유틸리티 도구를 추가할 수 있는 addToBelt 메서드와, 해당 객체가 가진 모든 유틸리티 도구를 관리하는 utilities 메서드를 제공합니다.
- addToBelt(name) 메서드는 인자로 받은 name을 객체의 _utility_items 배열에 추가합니다. 객체가 해당 배열을 아직 가지고 있지 않으면, 새로운 배열을 생성합니다. 메서드는 this를 반환하여 메서드 체이닝을 지원합니다.
- utilities() 메서드는 객체의 _utility_items 배열을 반환합니다. 이 배열은 객체가 가진 모든 유틸리티 도구를 저장합니다. 배열이 초기화되지 않았으면, 새로운 배열을 생성하여 반환합니다.
이제 `@` 구문을 mixin 함수의 이름과 함께 사용하여 `ComicBookCharacter`를 원하는 동작으로 데코레이션할 수 있습니다. 클래스 앞에 여러 개의 데코레이터 문을 접두사로 붙이는 것을 주목하세요:
@SuperPowers
@UtilityBelt
class ComicBookCharater {
constructor(first, last) {
this.firstName = first;
this.lastName = last;
}
realName() {
return this.firstName = ' ' + this.lastName;
}
};
이제 정의한 내용을 사용하여 배트맨 캐릭터를 제작해 보겠습니다.
const batman = new ComicBookCharater('Bruce', 'Wayne');
console.log(batman.realName());
// Bruce Wayne
batman
.addToBelt('batarang')
.addToBelt('cape');
console.log(batman.utilities());
// ['batarang', 'cape']
batman
.addPower('detective')
.addPower('voice sounds like Gollum has asthma');
console.log(batman.powers());
// ['detective', 'voice sounds like Gollum has asthma']
클래스용 데코레이터는 비교적 간결해서 함수 호출의 대안으로 사용하거나 상위 컴포넌트의 헬퍼로도 사용할 수 있습니다.
2. 클래스, 메서드, 속성 별 데코레이션 방법
다시 한 번 3가지 방식마다 데코레이팅 방식이 어떻게 다른지 비교하여 살펴보도록 하겠습니다.
2-1. 클래스 필드 데코레이팅
아래 예제에서는 클래스 필드에서 데코레이터 구문을 사용하는 방법을 볼 수 있습니다:
function locked(target, key, descriptor) {
return (
...descriptor,
writable: false,
};
}
class Data {
@ locked
password = 'mypwd';
}
위 코드에는 비밀번호 클래스 필드에 적용하는 locked라는 데코레이터 함수가 있습니다. 이 함수는 target, key, descriptor라는 세 가지 매개변수를 받습니다. target은 데코레이션할 객체 또는 함수이고 key는 해당 대상의 속성 이름을 나타냅니다. descriptor에는 꾸미고 있는 속성을 포함하여 대상의 모든 속성이 포함됩니다.
클래스 필드 데코레이터는 새 descriptor를 반환하는 방식으로 작동합니다. 따라서 writable 플래그를 false로 설정한 것을 제외하고 원본의 모든 속성과 값이 포함된 새 descriptor 객체를 반환합니다. 이렇게 하면 비밀번호 필드가 인스턴스화된 후에는 변경할 수 없습니다.
이제 비밀번호 필드를 변경해 보겠습니다:
const data = new Data();
data.password = 'hacked'; // Error: Cannot assign to readonly property 'password' of object '#<Data>'
보시다시피 비밀번호 필드를 변경하려고 하면 이제 readonly 속성이므로 오류가 발생합니다.
2-2. 클래스 메서드 데코레이팅
메서드에 데코레이터를 사용하는 예를 살펴보겠습니다:
function logMessage(target, name, descriptor) {
const original = descriptor.value;
const fn = function (...args) {
try {
const result = original.apply(this, args);
return result;
} catch (error) {
console.log(`Error: ${error}`);
throw error;
}
};
return { ...descriptor, fn };
}
class User {
@logMessage
getData = () => {
// API request here
throw new Error('Something went wrong');
};
}
const user = new User();
user.getData(); // "Error: Something went wrong"
데코레이팅 된 메서드는 descriptor를 취하고 원래 메서드와 추가 로직을 포함하는 객체를 반환합니다.
이 예시에서는 getData 메서드에 몇 가지 오류 처리를 추가하고 있습니다. 요청에 문제가 발생하면 이를 포착하여 콘솔에 기록합니다.
2-3. 클래스 데코레이팅
아래에서 전체 클래스에 데코레이터를 적용하는 방법을 확인할 수 있습니다:
function storeInCache(target) {
return (...args) => {
const result = new target(...args);
addToCacheMap();
return result;
};
}
@storeInCache
class User {
// ...
}
const user = new User(); // 자동으로 캐시에 저장됨.
storeInCache 데코레이터가 클래스에 적용되면 새 User 인스턴스가 캐시 맵에 자동으로 저장됩니다.
결론
자바스크립트 데코레이터는 매우 깔끔하고 읽기 쉬운 방식으로 여러 곳에 적용할 수 있는 간단한 헬퍼 코드를 작성하는 좋은 방법을 제공하여 시간을 절약하고 코드의 중복성을 줄여주는 강력한 기능입니다
'언어 > 자바스크립트(JavaScript)' 카테고리의 다른 글
npm과 yarn을 비교한 글 (1) | 2024.11.20 |
---|---|
[JavaScript] JavaScript에서 우아하게 에러 핸들링(Error Handling)하기 (try-catch) (2) | 2024.11.18 |
[TypeScript] 타입스크립트 interface vs. type (0) | 2024.05.08 |
[JavaScript] 자바스크립트 Node.js로 알아보는 디자인 패턴들(Design Patterns in JavaScript) (0) | 2024.03.08 |
[JavaScript] 함수 스코프(Function Scope)와 클로저(Closure) (0) | 2024.02.07 |
소중한 공감 감사합니다