Jieunny의 블로그
Section8. 데코레이터 본문
📣 강의 내용 정리
✔️ 데코레이터란?
➰ 클래스 선언과 멤버에 대한 주석과 메타 프로그래밍 구문을 모두 추가할 수 있는 방법을 제공하는 기능으로, 데코레이터 함수는 인자로 전달받은 함수의 동작을 수정한다.
➰ 쉽게 말해 데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나
➰ 매개변수, 속성, 접근자, 메소드, 클래스에 첨부할 수 있는 특별한 선언 함수
➰ 데코레이터가 어디에 장식되느냐에 따라서, 데코레이터 함수로 넘어오는 인자의 개수나 리턴값이 달라진다
(데코레이터의 종류가 달라지는 것)
✚ 메타프로그래밍이란?
➰ 자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성, 수정하는 것을 말한다.
𝟭. 클래스 데코레이터
function Logger(constructor: Function) {
console.log("Logging...");
console.log(constructor);
}
@Logger
calss Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
const pers = new Person();
console.log(pers);
//Logging...
//Class Person {
// ...
// }
//Creating person object...
//Person {name:'Max'}
➰ 첫문자는 대문자, 소문자 모두 가능하지만 보통 대문자로 많이 쓴다.
➰ 클래스 데코레이터를 이용하면 클래스에서 정의하지 않은 속성을 정의할 수도, 이미 클래스에서 부여한 값을 바꿀 수도 있다.
➰ 데코레이터는 실체화되기 전 클래스가 정의만 되도 실행된다.
➰ 클래스 데코레이터의 인자는 constructor(TS가 클래스를 실행할 때 클래스의 constructor를 데코레이터의 파라미터로 자동 전달하므로 명시적으로 전달하지 않아도 된다)
➰ 리턴값은 class나 void 이다.
𝟮. 데코레이터 팩토리 작업하기
✔️ 데코레이터를 만드는 대신, 데코레이터 팩토리(factory)를 정의할 수 있다.
✔️ 데코레이터 팩토리 함수 : 데코레이터를 감싸는 wrapper 함수로, 보통 데코레이터 함수가 사용자로부터 인자를 전달받을 수 있도록 설정 할 때 사용한다.
function Logger(logString: string) { // 팩토리 함수
return function(constructor: Function) { // 실제 데코레이터
console.log("Logging...");
console.log(constructor);
}
}
@Logger('LOGGING - PERSON')
calss Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
➰ @Logger 부분을 데코레이터 함수 대신 사용할 수 있다 -> 기본 로깅 문자 대신 내가 전달한 문자열을 보여준다.
➰ 팩토리 함수와 함께 실행된다면 데코레이션 함수가 사용하는 값을 커스터마이즈 할 수 있다.
𝟯. 더 유용한 데코레이터 만들기
function WithTemplate(template: string, hookId: string) {
return function(_: Function) {
// _ : 존재는 알지만 입력하지 않겠다고 명시하는 것
const hookEl = document.getElementById('hookId');
if(hookEl){
hookEl.innerHTML = template;
}
}
}
@WithTemplate('<h1>My Person Object</h1>', 'app')
// HTML 파일에 <div id="app"></div> 선언해준 상태
➰ 화면에 <h1> 속성으로 My Person Object 문자열이 나타난다.
function WithTemplate(template: string, hookId: string) {
return function(constructor: any) {
const hookEl = document.getElementById('hookId');
const p = new constructor();
if(hookEl){
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = p.name;
}
}
}
@WithTemplate('<h1>My Person Object</h1>', 'app')
// HTML 파일에 <div id="app"></div> 선언해준 상태
calss Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
const pers = new Person();
console.log(pers);
➰ 화면에 <h1> 속성으로 Max 가 나타난다.
𝟰. 데코레이터 추가하기
✔️ 데코레이터가 나타나는 순서
➰ 하나의 멤버에 동시에 여러 개의 데코레이터를 장식할 수 있다.
➰ 각 데코레이터의 팩토리 함수가 먼저 실행되고(결과는 위에서 아래로), 실제 데코레이터 함수 나중에 (결과는 아래에서 위로)
function Logger(logString: string) {
console.log("Logger factory"); // 첫번째
return function (_: Function) {
console.log(logString); // 네번째
};
}
function WithTemplate(template: string) {
console.log("template factory"); // 두번째
return function (_: Function) {
console.log(template); // 세번째
};
}
@Logger("LOGGING")
@WithTemplate("Rendering Template")
𝟱. 속성(프로퍼티) 데코레이터
function writable(isWritable: boolean) {
return function (target: any, propertyName: any): any {
return {
writable,
};
};
}
class Test {
property = 'property';
@writable(true)
public data1 = 0;
@writable(false)
public data2 = 0;
}
const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.
➰ 프로퍼티의 설정을 바꿀 수 있다.
➰ 프로퍼티 데코레이터의 첫번째 인자는 static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 프로토타입 객체
➰ 두번째 인자는 해당 프로퍼티의 이름
➰ getter, setter를 설정해서 값을 받아올 수도, 설정할 수도 있다.
𝟲. 접근자 & 매개변수 데코레이터(??)
✔️ 접근자 데코레이터 : 접근자 앞에 사용하며 접근자의 속성 설명자에 적용되고 접근자의 정의를 읽거나 수정할 수 있다
➰ 접근자 : 객체 프로퍼티를 객체 외에서 읽고 쓸 수 있는 함수(getter, setter)
➰ 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
➰ 두번째 인자: 해당 property/method의 이름
➰ 세번째 인자: 해당 property/method의 descriptor
➰ 리턴값: 새로운 Property Descriptor, void
function Enumerable(enumerable: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = enumerable;
};
}
class Person {
constructor(private name: string) {}
@Enumberable(true)
get getName() {
return this.name;
}
@Enumberable(false)
set setName() {
this.name = name;
}
}
const person = new Person('hello');
for (let key in person) {
console.log(`${key}: ${person[key]}`);
}
//결과를 출력하면 getName은 출력되지만 setName은 열거하지 못하게 되었기 때문에 for 문에서 key로 받을 수 없다.
✔️ 매개변수 데코레이터 : 메서드의 매개 변수에 선언되며 클래스 생성자 함수나 메서드의 매개변수로 사용할 수 있다.
➰ 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
➰ 두번째 인자: 매개변수가 들어있는 메소드의 이름
➰ 세번째 인자: 메소드 내 파라미터의 index(0부터 시작)
➰ 리턴값은 무시된다.
// Log 매개변수 데코레이터
function Log(t:any, p:string, i:number) {
console.log(t.name);
console.log(`
매개변수 순서: ${i},
멤버 이름: ${p === undefined ? 'constructor' : p}
`);
}
// Button 클래스
class Button {
el:HTMLButtonElement;
color:string;
// 생성자 함수 내에 Log 데코레이터 사용
constructor(
@Log el:HTMLButtonElement,
@Log color:string = 'transparent'
) {
this.el = el;
this.color = color;
}
// 스태틱 메서드 내에 Log 데코레이터 사용
static initialize(
@Log els:NodeList,
@Log color:string = 'transparent'
){
return [].slice.call(els).map(el => new Button(el, color));
}
}
// Button.initialize() 스태틱 메서드 사용
const btns = Button.initialize( document.querySelectorAll('.button'), '#900' );
//console
Button
매개변수 순서: 1, 멤버 이름: initialize
Button
매개변수 순서: 0, 멤버 이름: initialize
Button
매개변수 순서: 1, 멤버 이름: constructor
Button
매개변수 순서: 0, 멤버 이름: constructor
➰ 전달 받은 인자 중 마지막 매개변수 인덱스는 클래스 생성자 또는 메서드 매개변수 내 데코레이터를 사용한 순서
➰ 매개변수의 순서가 역순이다.
📚 https://typescript-kr.github.io/pages/decorators.html
𝟳. 데코레이터는 언제 실행하는가
✔️ 프로퍼티, 메소더, 액세서, 매개 변수 등 어떤 데코레이터든 클래스를 정의할 때 실행된다.
➰ 데코레이터는 클래스가 정의될 때 뒤에서 부가적인 작업을 하는 것
➰ 데코레이터는 JS에서 클래스 및 constructor 함수만 정의되면 인스턴스화하지 않아도 실행된다.
𝟴. 데코레이터 반환 타입
✔️ 클래스 데코레이터 : 새 함수, 컨스트럭터 함수, 새 클래스를 반환할 수 있다.
✔️ 프로퍼티 데코레이터 : 리턴값은 무시된다.
✔️ 접근자 & 메서드 데코레이터 : 새로운 Property Descriptor, 함수를 반환할 수 있다.
✔️ 매개변수 데코레이터 : 리턴값은 무시된다.
𝟵. "Autobind" 데코레이터 만들기
✔️ Autobind 데코레이터 : this를 자동으로 주변의 클래스에 바인딩 해준다.
//index.html
<button> Click me </button>
✔️ 직접 바인딩 해주는 경우
//app.ts
class Printer {
message = 'This works!';
showMessage() {
console.log(this.message);
}
}
const p = new Printer();
p.showMessage(); // this가 Printer를 참조
const button = document.querySelector('button')!;
button.addEventListener('click', p.showMessage); // this가 이벤트의 대상을 참조
// event가 발생했을 때 this가 참조하고 있는 값이 showMessage함수가 참조하고 있는 값과 다르기 때문이 실행되지 않는다.
button.addEventListener('click', p.showMessage.bind(p));
//showMessage 대신 p를 참조하여 다시 Printer를 참조하게 만든다.
✔️ Autobind 데코레이터 사용
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) { // 데코레이터
const originalMethod = descriptor.value;
const adjDescriptor = { // 나중에 반환될 값
configurable: true,
enumerable: false, // for in 루프 표시 안함.
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
} // getter 계층 추가하기
// 여기서 this는 this가 속해 있는 확실한 객체(원래 우리가 정의한 메서드에 있는 객체를 참조한다.
}
return adjDescriptor;
}
// 새로운 디스크립터 객체를 반환하면, 그 디스크립터 객체가 이전의 디스크립터를 덮어쓴다.
// -> 이전의 매서드 디스크립터를 대체한다.
// -> 이전의 메서드 구성을 새로운 구성으로 대체하고(get()), 그러면 getter계층이 추가되는 것이다.
class Printer {
message = 'This works!';
@Autobind
showMessage() {
console.log(this.message);
}
}
class p = new Printer();
const button = document.quertSelector('button')!;
button.addEventListener('click', p.showMessage);
// 이제 직접 바인딩 하지 않아도 작동한다!
➰ 메소드 데코레이터를 통해 기존의 메소드를 대체해 해당 메소드가 지칭하는 this의 값이 항상 인스턴스를 향하도록 조정
𝟭𝟬. 유효성 검사 데코레이터
//index.html
<form>
<input type="text" placeholder="Course title" id="title">
<input type="text" placeholder="Course price" id="price">
<button type="submit">Save</button>
</form>
// app.ts
interface ValidatorConfig {
[property: string] : {
[validatableProp: string]: string[] // ['required', 'positive']
}
}
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
// registeredValidators에 있는 해당 클래스 이름의 기존 키-값 쌍을 가져온다.
[propName]: [...registeredValidators[target.constructor.name]?.[propName] ?? []), 'required']
};
}
// 데코레이터
function PositiveNumber(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [propName]: [...registeredValidators[target.constructor.name]?.[propName] ?? []), 'positive']
};
}
// 데코레이터
function validate(obj: object) {
const objValidatorConfig = registeredValidators[obj.constructor.name];
if(!objValidatorConfig) {
// 분석 할 객체가 없을 때
return true;
}
let isValid = true; // 모든 프로퍼티가 유효성 검증이 되었는지 표시하는 변수
for(const prop in objValidatorConfig){
console.log(prop);
// 프로퍼티에 대해 있을 수 있는 모든 validator를 검사한다.
for(const validator of objValidatorConfig[prop]{
// positive 나 required 같은 것들을 가져오게 되고, 이들은 모두 validator 상수에 저장된 것들.
switch(validator) {
case 'required':
isValid = isValid && !!obj[prop];
break;
case 'positive':
isValid = isValid && obj[prop] > 0;
break;
}
}
}
return isValid;
}
// 우리는 어떤 객체든 분석할 수 있고, 타입스크립트는 그 객체를 보고 클래스 이전에 우리가 등록한
// 유효성 검사를 찾아서 로직을 적용할 것이다.
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}
const courseForm = document.querySelector('form')!;
courseForm.addEventListener('submit', event => {
event.preventDefault();
const titleEl = document.getElementById('title') as HTMLInputElement;
const priceEl = document.getElementById('price') as HTMLInputElement;
const title = titleEl.value;
const price = +priceEl.value;
// + 붙이면 Number로 전환됨.
const createdCourse = new Course(title, price);
if(!validate(createdCourse)) {
alert('Invalid input, please try again!');
return;
}
console.log(createdCourse);
});
💡 새로 알게 된 점
📚 데코레이터를 잘 정리해 놓은 블로그 : https://velog.io/@zeroequaltwo/TypeScript-%EA%B0%95%EC%9D%98-%EC%A0%95%EB%A6%AC-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0
1. 데코레이터가 어떤 것인지, 어떤 종류가 있는지, 왜 사용하는지
2. 근데 너무 복잡해서 아직 이걸 사용하는게 정말 편리한지 모르겠음..
'CodeStates > TS 스터디' 카테고리의 다른 글
Section10. 모듈 및 네임스페이스 (0) | 2023.03.23 |
---|---|
Section9. Drag & Drop 프로젝트 만들기 (2) | 2023.03.20 |
Section 7. 제네릭 (0) | 2023.02.22 |
Section 6. 고급 타입 (0) | 2023.02.21 |
Section 5. 클래스 & 인터페이스 (0) | 2023.02.21 |