VueJS에서 Typescript로 개발하기

이번에는 좀 다른 이야기를 해볼까 합니다.
VueJS 프로젝트를 vue-cli를 사용해 Typescript를 사용하도록 생성하면 기본으로 들어가 있는 vue-property-decorator 를 사용하면 VueJS를 Typescript와 클래스 형태로 사용하기 좋지만, 그대로 사용하게 되면 코드들이 완전히 클래스 형태로 사용한다는 느낌은 좀 덜 받게 됩니다.

아래 코드를 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div class="home">
<p>{{count}} * 2 = {{doubledCount | answer}}</p>
<button @click="incr(1)">incr</button>
</div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { mapState, mapActions } from 'vuex';

@Component({
computed: {
...mapState('CountStore', {
count: 'count',
}),
},
methods: {
...mapActions('CountStore', {
incr: 'incr',
}),
},
filters: {
answer: (n: number) => `${n} 입니다!`,
},
})
export default class Home extends Vue {

private count!: number;

private incr!: (delta: number) => void;

}
</script>

물론, 지난 포스트에 올렸던 코드는 vuex-class의 도움을 받아 vuex와 컴포넌트를 연결할 때 사용하는 조금은 장황한 mapState, mapActions 대신 데코레이터로 코드를 바꿀 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div class="home">
<p>{{count}} * 2 = {{doubledCount}}</p>
<button @click="incr(1)">incr</button>
</div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { namespace, State, Action, Getter } from 'vuex-class';

const CountStoreModule = namespace('CountStore');

@Component({
filters: {
answer: (n: number) => `${n} 입니다!`,
},
})
export default class Home extends Vue {

@CountStoreModule.State('count')
private count!: number;

@CountStoreModule.Getter('doubledCount')
private doubledCount!: number;

@CountStoreModule.Action('incr')
private incr!: (delta: number) => void;

}
</script>

그러나 여전히 마음에 안드는 부분이 있습니다. filters 부분인데요 얘네들도 결국엔 function 들인데 클래스 안쪽에서 정의할 수는 없을까? 하는 생각이 들었습니다.
(Filter에 대한 설명은 vue공식페이지에서 참조하시기 바랍니다.)

물론 이를 해결해주는 라이브러리가 당연히 있습니다.
vue-ts-decorate라는 녀석인데요, 사용하려고 고려해 보았으나, last publish가 2년 전입니다. 시시각각 변하는 frontend 생태계에서 마지막 업데이트가 2년전이라면 죽은 프로젝트라고 보아도 될 것 같다는 판단이네요.
그리고 또 한가지 마음에 들지 않는 점은, Filter뿐만 아니라 기존에 vue-property-decorator 가 제공하는 데코레이터들(Prop, Watch 등)도 중복해서 지원하고, 이를 적용하기 위해선 기존 코드들도 vue-ts-decorate를 적용하도록 변경해야 하는 것 처럼 보였습니다.

vue-ts-decorate 말고는 @Filter 데코레이터를 지원하는 라이브러리는 없는것 처럼 보입니다. (혹시 알고계신 분 있으시면 제보좀 부탁드립니다.)

그래서 더이상 검색은 포기하고, 직접 만들기로 합니다.

/src 폴더에 utils 폴더라는 폴더를 만들고, Decorators.ts 라는 이름의 파일을 생성 합니다.

그리고 아래 내용을 붙여넣습니다.

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue';

type VueClass<V> = (new (...args: any[]) => V & Vue) & typeof Vue;

export function Filter(): any {
return (target: Vue, propertyKey: string, descriptor: PropertyDescriptor): any => {
const Ctor = target.constructor as VueClass<Vue>;
Ctor.filter(propertyKey, descriptor.value);
};
}

별로 길지 않은 간단한 코드지만, 이 코드를 추가하면 이제 @Filter 데코레이터를 통해 필터를 정의할 수 있게 됩니다.

자, 이제 Filter 데코레이터를 정의 했으니, 기존 코드를 수정해 @Component 데코레이터 안에 (클래스 밖에) 정의되어 있던 Filters를 클래스 내부로 옮겨봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="home">
<p>{{count}} * 2 = {{doubledCount | answer}}</p>
<button @click="incr(1)">incr</button>
</div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { namespace, State, Action, Getter } from 'vuex-class';
import { Filter } from '@/utils/Decorators';

const CountStoreModule = namespace('CountStore');

@Component
export default class Home extends Vue {

@CountStoreModule.State('count')
private count!: number;

@CountStoreModule.Getter('doubledCount')
private doubledCount!: number;

@CountStoreModule.Action('incr')
private incr!: (delta: number) => void;

@Filter()
private answer(n: number) {
return `${n} 입니다!`;
}

}
</script>

새로 정의한 Filter 데코레이터를 import 해주고, import { Filter } from '@/utils/Decorators'; 클래스 내부에 @Filter() 데코레이터와 함께 필터의 내용을 작성해 줍니다. (여기서 주의할 점은 필터는 순수함수로만 만들어져야 합니다. this 키워드를 통해 함수 외부의 변수는 사용할 수 없습니다.)

filter가 잘 작동하는 것을 확인할 수 있습니다.