Angular에 Custom Decorator 적용하기
in Javascript
예전부터 회사에서 ngrx(redux 패턴)
+ rxjs
혼합 사용이 어렵다는 의견이 자주 나와서 ngrx를 걷어내고 있다. 사실 Angular가 워낙 잘되어있어 굳이 ngrx
까지 사용할 이유는 없었지만, 예전에 redux가 핫하다는 이유 하나만으로 적용한 게 발목을 잡고 있다.
ngrx
를 사용하는 대신 간단한 State 클래스를 만들고 여기서 상태를 관리하려고 하는데, getter/setter 메소드 때문에 반복적인 코드가 많아졌다.
// todo.state.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface ITodoItem {
id: string;
name: string;
createdAt: Date;
}
@Injectable()
export class TodoState {
private totalSubject = new BehaviorSubject<number>(0);
private todoListSubject = new BehaviorSubject<ITodoItem[]>([]);
private todoItemSubject = new BehaviorSubject<ITodoItem | null>(null);
get total$(): Observable<number> {
return this.totalSubject.asObservable();
}
get total(): number {
return this.totalSubject.getValue();
}
setTotal(total: number) {
this.totalSubject.next(total);
}
get todoList$(): Observable<ITodoItem[]> {
return this.todoListSubject.asObservable();
}
get todoList(): ITodoItem[] {
return this.todoListSubject.getValue();
}
setTodoList(list: ITodoItem[]) {
this.todoListSubject.next(list);
}
get todoItem$(): Observable<ITodoItem | null> {
return this.todoItemSubject.asObservable();
}
get todoItem(): ITodoItem {
return this.todoItemSubject.getValue();
}
setTodoItem(info: ITodoItem) {
this.todoItemSubject.next(info);
}
}
Observable 형태를 지원하려고 상태를 저장할 변수를 Subject
타입으로 선언하니까 하나의 상태당 3개의 메소드를 작성해야 했다.
위 코드처럼 단순히 몇 개의 상태만 관리하는데도 코드가 반복적이고 길어져서 마음에 안들었다. 이 코드를 줄일 방법을 고민하다가 Decorator
를 사용하기로 했다. 커스텀 데코레이터로 아래 코드처럼 사용할 수 있도록 만들었다.
@Injectable()
export class TodoState {
@State(0)
total: number;
@State([])
todoList: ITodoItem[];
@State()
todoItem: ITodoItem;
}
1. tsconfig.json 수정
custom decorator를 사용하기 위해 Angular 프로젝트에서 app
폴더 아래에 decorators
폴더를 만들고 컴파일러 옵션과 path를 수정한다. compilerOptions
에서 experimentalDecorators
, emitDecoratorMetadata
옵션을 추가하고, paths
에 decorators 폴더 경로를 추가한다.
// tsconfig.json
"compilerOptions": {
...
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
...
"paths": {
...
"@decorators/*": ["app/decorators/*"],
...
},
}
2. State Decorator
타입스크립트의 Property Decorator
를 사용하면 변수에 부가적인 설정을 할 수 있다.
이걸 이용해 getter/setter를 만들어주는 State
데코레이터를 작성했다. 아이디어는 변수이름$
형태로 observable 형태의 getter/setter를 추가해주고, set변수이름
라는 별도의 함수를 만드는 것이다.
우선 데코레이터 팩토리 형태로 함수를 만들어준다. 첫 번째 State 함수는 데코레이터의 paramter, 리턴 함수에서는 데코레이터의 대상과 변수 이름을 받는 형태다.
// state.decorator.ts
function State<T>(initValue: T | null = null) {
return function(target: object, property: string) {
// TODO: define getter/setter
}
}
다음은 getter/setter를 만들어준다.
https://dev.to/danywalls/using-property-decorators-in-typescript-with-a-real-example-44e
// state.decorator.ts
import { BehaviorSubject } from 'rxjs';
interface ICustomKey {
accessKey: string;
customKey: string;
}
const getAccessAndCustomKey = (key: string): ICustomKey => {
return {
accessKey: `${key}$`,
customKey: `____${key}$`,
};
};
export function State<T>({ initValue = null, loggable = false } = {}) {
return function(target: object, property: string) {
const { accessKey, customKey } = getAccessAndCustomKey(property);
Object.defineProperty(target, accessKey, {
get() {
if (this[customKey]) {
return this[customKey];
}
this[customKey] = new BehaviorSubject<T| null>(initValue);
return this[customKey];
},
set() {
throw new Error(`cannot set ${accessKey}`);
},
});
Object.defineProperty(target, property, {
get() {
return this[accessKey].getValue();
},
set(value: T) {
this[accessKey].next(value);
},
});
// add set function
const setterName = `set${property[0].toUpperCase()}${property.slice(1)}`;
target[`${setterName}`] = function(value: T) {
this[accessKey].next(value);
};
};
}
위 코드는 Github에서 확인할 수 있다.