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가 잘 작동하는 것을 확인할 수 있습니다.

Comment and share

Vuex

VueJS를 사용하면서 상태를 관리하는 대표적인 방법은 Vuex를 사용하는 방법이 있습니다.(Vue에서 공식적으로 추천하는 상태관리 라이브러리 이기도 합니다.) 이번 포스트에서는 Vuex를 Typescript 적인 방법으로 사용하는 방법에 대해서 작성하도록 하겠습니다.

Vuex에 대한 사전 지식은 공식사이트에 자세히 설명이 되어 있습니다. (한글로 말이죠..)
그러나 이곳에 나와있는 방법으로는 Typescript와는 무엇인가 입맛에 잘 맞지 않습니다.

우리는 vuex를 클래스 형태로 만들어서 사용하기로 합시다.

vue-cli를 통해 프로젝트를 생성 하면서 vuex를 선택하게 되면 src/store.ts 라는 파일에 store 가 정의가 됩니다.

우리는 이 store.ts 파일을 지우고, stores 폴더를 만들어 store를 각각 namesapce 별로 모듈화 하여 분리하고, 각각의 스토어 모듈은 클래스 형태로 정의해서 쓰도록 합시다.

이를 위해 우리는 vuex-module-decoratorsvuex-class 라는 라이브러리를 사용합니다.

터미널에서 yarn add 명령어를 통해 라이브러리를 추가해 줍니다.

1
2
yarn add vuex-module-decorators
yarn add vuex-class

src 폴더 하단에 stores 폴더를 만들고, index.ts 파일을 새로 생성합니다.

그리고 기존 store.ts파일을 import 하고 있던 main.ts 파일을 열어 아래와 같이 바꿉니다.

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './stores'; //이부분.'./store' -> './stores'

Vue.config.productionTip = false;

new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

그리고 /src/stores/index.ts 파일을 열어 아래와 같이 내용을 넣습니다.

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

Vue.use(Vuex);

export default new Vuex.Store({
modules: {

},
});

기본적인 틀은 셋팅이 끝났습니다. 우리는 이제 원하는대로 클래스 스타일 스토어 모듈을 작성하고 modules 밑에 넣어주기만 하면 됩니다.

Store 모듈 생성

/src/stores/CountStore.ts 파일을 만들고 아래 내용을 넣습니다.

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
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';

@Module({namespaced: true})
export default class CountStore extends VuexModule {

// states
public count: number = 0;

// getters
get doubledCount() {
return this.count * 2;
}

// mutations
@Mutation
public increment(delta: number) {
console.log(`increment mutation: ${delta}`);
this.count += delta;
}

// actions
@Action({ commit: 'increment' })
public incr(delta: number) {
console.log(`increment action: ${delta}`);
return delta;
}

}

state, getter, mutation, action 들은 위와 같은 형식으로 적용하여 사용합니다. 컴포넌트에서 incr 액션함수를 호출하면, 함수가 return 하는 delta라는 값으로 increment mutation을 commit 하게 됩니다. 설명이 약간 어설픈데요, 결국 위에 선언한 incr 이라는 이름의 Action 은 아래 코드와 동일합니다. (namespace를 사용하지 않으려면 @Module 데코레이터의 내용을 빼고 작성합니다.)

1
2
3
4
5
actions: {
incr: (context: any, delta: number) => {
context.commit('increment', delta);
},
}

/src/stores/index.ts 파일을 열어 방금 만든 CountStore 모듈을 추가하는 구문을 넣어줍니다.

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue';
import Vuex from 'vuex';
import CountStore from './CountStore';

Vue.use(Vuex);

export default new Vuex.Store({
modules: {
CountStore,
},
});

이제 우리는 CountStore 라는 StoreModule을 완성했습니다.
이제는 우리가 원하는 컴포넌트에서 CountStore를 연결하여 사용할 차례입니다.

Home.vue 파일을 열어 아래와 같이 바꿔줍니다.

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
<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
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>

CountStoreModule을 namespace를 통해 연결하고, state와 getter를 통해 상태값을 가져와 화면에 바인딩 합니다. 그리고 버튼을 incr 액션 생성함수와 연결 해줍니다.

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

이 외에 async를 활용한 @MutationAction도 있습니다. 이에 대한 상세한 설명은
vuex-module-decorators github 페이지를 참조하시기 바랍니다.

부족한 글 읽어주셔서 감사합니다.

Comment and share

  • page 1 of 1

shockshot@naver.com

author.bio


author.job