Angular에 Custom Decorator 적용하기

Typescript Property Decorator로 반복 코드 줄이기

예전부터 회사에서 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에서 확인할 수 있다.


© 2019. All rights reserved.