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

vue-property-decorator 에 대해 알아봅시다.

vue-property-decorator 는 vuejs 에서 typescript로 개발할 때, 클래스 컴포넌트 스타일로 개발하기 쉽게 도와주는 데코레이터 들입니다. 제공해 주는 데코레이터들로는

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component

가 있습니다. 이 중 @Component 데코레이터와 @Prop 데코레이터는 지난 포스팅에서 살펴 봤습니다.

그럼 우리가 Vue와 typescript를 사용하면서 제일 많이 사용하게 될 @Prop 데코레이터부터 살펴보도록 하겠습니다.

@Prop()

1
@Prop(options: (PropOptions | Constructor[] | Constructor) = {}) decorator

출처: https://github.com/kaorun343/vue-property-decorator

위와 같은 형식으로 사용하며,

1
2
3
4
5
6
7
8
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Prop(Number) propA!: number
@Prop({ default: 'default value' }) propB!: string
@Prop([String, Boolean]) propC!: string | boolean
}

이렇게 만들어진 코드는 아래 코드와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
props: {
propA: {
type: Number
},
propB: {
default: 'default value'
},
propC: {
type: [String, Boolean]
},
}
}

default 옵션을 통해 기본값을 지정할 수도 있습니다.

@Emit()

Emit 데코레이터는 자식 -> 부모 컴포넌트로 이벤트 바인딩을 할때 주로 사용합니다.

1
@Emit(event?: string)

형식으로 선언되며, Emit 하고자 하는 event 명을 string 형식으로 넣어 선언하면 됩니다.

1
2
@Emit('change')
private onChange(value: string) {}

과 같이 선언하며, ‘change’와같이 emit되는 이벤트 명을 넣지 않을 경우, 함수 명을 이벤트 명으로 사용하게 됩니다. (단, 이벤트명을 넣지 않아 자동으로 이벤트 emit이 선언된 경우에는 카멜 케이스의 함수명이 케밥 케이스의 함수명으로 변경되어 적용됩니다. 예: onChange -> on-change)

우리가 생성했던 test-project에 Emit을 사용한 코드를 넣어 봅시다.

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">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld
msg="Welcome to Your Vue.js + TypeScript App"
@onClick="clickEventHandler"
/>
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {

private clickEventHandler() {
console.log('click!');
}

}
</script>

HelloWorld에 onClick 이라는 이름으로 clickEventHandler 함수를 바인딩 했습니다.
그리고 클래스에 clickEventHandler() 라는 함수를 정의 했습니다. 해당 함수가 실행 되면 콘솔 창에 click!이라는 메세지를 출력합니다.

HelloWorld.vue 파일을 열어 내용을 아래와 같이 바꿉니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button @click="onClick" >클릭!</button>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;

@Emit('onClick')
private onClick() {
//
}
}
</script>

.hello 클래스를 가진 div에 클릭을 할 경우 onClick함수를 실행시키는 버튼을 추가 했습니다.
그리고 onClick() 함수 앞에 Emit(‘onClick’) 이라고 Emit 데코레이터를 선언 했습니다. 이제 이 onClick() 함수가 실행되면 부모 클래스로 onClick 이벤트를 Emit 합니다.

이 Emit 데코레이터는 @Model 데코레이터와 함께 많이 사용되며, 이벤트 바인딩을 할때 필수기 때문에 잘 봐두어야 합니다.

만약, event emitting과 함께 데이터를 전달하고 싶다면??

Home.vue와 HelloWorld.vue 클래스를 아래와 같이 바꿔봅시다.

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">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld
msg="Welcome to Your Vue.js + TypeScript App"
@onClick="clickEventHandler"
/>
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {

private clickEventHandler(message: string) {
console.log(message);
}

}
</script>

HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button @click="(event) => onClick('클릭이벤트가 발생했했습니다.')" >클릭!</button>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;

@Emit('onClick')
private onClick(message: string) {
//
}
}
</script>

선언하는 함수의 args를 전달하고 싶은 데이터 형식으로 넣어 전달하면 됩니다.

@Model()

Model 데코레이터는 @Prop 데코레이터 다음으로 많이 쓰이게 될 데코레이터가 아닌가 합니다. Prop은 일반적으로 단방향 바인딩인데 반해, Model은 양방향 바인딩을 제공합니다.

1
@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {}) decorator

선언방법은 위와 같습니다.

1
2
3
4
5
6
import { Vue, Component, Model } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Model('change', { type: Boolean }) checked!: boolean
}

위 코드는 아래와 같은 코드와 동일한 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: {
type: Boolean
},
},
}

이제 우리의 test project에 코드를 만들어봅시다.

Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="home">
{{count}}
<HelloWorld v-model="count"/>
</div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {

private count: number = 0;

}
</script>

count 변수를 새로 넣었고, 이를 화면에 출력하도록 했습니다.
그리고 HelloWorld 컴포넌트에는 count 변수를 v-model로 양방향 바인딩을 했습니다.

HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="hello">
<button @click="(event) => onChange(count+1)" >add</button>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Emit, Model } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {

@Model('change', {type: Number})
private count!: number;

@Emit('change')
private onChange(count: number) {}

}
</script>

HelloWorld 컴포넌트의 다른 내용은 지우고 버튼 하나만 남겨뒀습니다.
count 변수를 model로 받도록 @Model 데코레이터와 함께 선언했고, 버튼을 눌렀을 경우 받은 count에 +1을 해서 change event를 emit 하도록 선언 했습니다.

이제 컴파일 하고 버튼을 클릭 하면, Home.vue 컴포넌트에서 count값이 1씩 올라가는 것을 확인할 수 있습니다.

@Watch

@Watch 데코레이터는 값이 변경되는지를 감시하고 있다가, 변경 됐을 때 같이 선언한 함수를 실행시켜주는 데코레이터 입니다.

1
2
3
4
@Watch('count')
onChangeCount(val: number, oldVal: number) {
//함수 내용
}

우리의 Home.vue에 count가 변경될 때 log를 찍어주는 내용을 추가해 봅시다.

1
2
3
4
@Watch('count')
private onChangeCount(val: number, oldVal: number) {
console.log(`${oldVal} -> ${val}`);
}

를 Home.vue에 추가해 주고 확인해 봅니다.

클릭할때마다 count 의 값이 변경되고, 이를 감지해 onChangeCount가 실행되는 것을 확인 할 수 있습니다.

이 외 데코레이터(@Inject / @Provide)

Inject와 Provide는 서비스 주입 형태 사용시 사용되게 되는데, 개인적인 의견으로는 vuex 패턴으로 프로젝트를 디자인 하게 될 경우, 사용빈도가 낮아지게 되어 별도로 설명하지는 않겠습니다.

(vue 공식 페이지에서도 provide와 inject는 주로 고급 플러그인/컴포넌트 라이브러리를 위해 제공되고, 일반 애플리케이션 코드에서는 사용하지 않는 것이 좋다고 설명되어 있습니다.)

자세한 설명

다음 포스트에서는 vuex를 typescript 클래스 형태로 사용하는 법에 대해서 작성하도록 하겠습니다.

감사합니다.

Comment and share

사전준비

지난번에는 vue-cli를 통해 프로젝트를 생성 해보았습니다.

이번에는 폴더 구조와 뷰/컴포넌트 구성을 해볼텐데요,

typescript를 지원하는 에디터를 미리 설치하셔서 사용하시기 바랍니다.

저는 vscode를 주로 사용하는데요, Typescript 문법을 완벽지원할뿐 아니라 플러그인을 통해 vuejs와의 궁합도 매우 좋아 추천하는 에디터입니다. 아톰 에디터나 intelliJ를 사용해도 문제 없으니 익숙한 도구로 사용하시면 좋을 것 같아요.

추천하는 플러그인
vscode의 plugin 영역에서 vue.js extension pack 로 검색하면 나오는
플러그인팩을 추천합니다. 이거 하나 설치하면 한번에 여러개(Auto Close Tag, Vetur 등등)가 설치가 되는데요, 같이 설치되는 플러그인 중 eslint는 제거하고, tslint로 사용하고 있어요.

에디터 셋팅이 끝났다면, 해당 에디터를 사용해서 생성된 project 폴더를 여시기 바랍니다.

물론 폴더 구조는 입맛에 따라 바꿔서 사용하기도 합니다.

1
2
3
4
5
6
7
8
9
10
src           - 소스 폴더입니다. 주로 다루게 될 폴더죠.
-assets - static한 이미지 등의 파일이 위치합니다.
-components - 컴포넌트들이 위치합니다. 화면을 구성하는 작은 조각들이라고 생각하시면 됩니다.
-views - 컴포넌트들을 모아 구성되는 화면들의 view라고 보면 됩니다. vuex를 사용하여 컴포넌트들에게 데이터를 바인딩하거나 액션을 보내기도 합니다.
-App.vue - 앱의 기본 컴포넌트입니다. 엔트리 포인트인 main.ts에서 호출하는 최초의 Vue 컴포넌트.
-main.ts - 앱이 호출되면 실행되는 엔트리 포인트입니다. 이 ts 파일에서 router, store, App.vue 파일들을 불러옵니다.
-router.ts - 앱의 라우팅 설정읻 들어간 파일입니다. 라우터에 대해서는 추후 설명할 예정입니다.
-store.ts - 앱의 상태관리를 담당하는 store 입니다.
package.json - 패키지매니저(npm 또는 yarn)에서 관리하는, dependency 관련 설정 파일입니다.
tsconfig.json - typescript 컴파일 관련 설정파일입니다.

이정도로만 간단히 필요한 파일들만 설명하고 넘어가도록 하겠습니다.

그럼, 이 프로젝트의 메인 컴포넌트인 App.vue를 열어보도록 하겠습니다.

상단의 <template> 태그로 html 형태로 구선된 템플릿 영역이 보이고, 하단에는 scss로 작성된 <style>이 보입니다.
template 내부에 HOME, ABOUT으로 라우터 링크가 걸려 있네요. 그리고 <router-view/> 태그도 보입니다. router에 대해서는 추후 설명하겠지만, router-view 태그는 router에서 설정된 대로 화면을 그려준다고 일단 설명 드리겠습니다.

/views/Home.vue 파일을 열어봅시다.

최상단에는 App.vue파일과 마찬가지로 <template>영역이 있습니다.
그리고 바로 하단에 <script lang=”ts”>영역이 있습니다. 이곳에 component를 구성하는 Typescript로 작성된 class들이 들어갑니다.

1
import { Component, Vue } from 'vue-property-decorator';

우리는 클래스 스타일 컴포넌트를 좀 더 편하게 사용하기 위해 vue-property-decorator를 사욯합니다.
@Component 데코레이터 외에 우리가 자주 사용하게 될 @Prop, @Emit 등의 데코레이터를 제공합니다.

1
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

그 바로 아랫줄엔 HelloWorld 컴포넌트를 import 하는 구문이 들어가 있습니다. vue 파일을 import 할 때, ‘@/component/‘ 형식으로 절대경로 참조가 가능합니다. 혹은 ‘../components/HelloWorld.vue’와 같은 형식으로 상대경로 참조도 가능합니다.

1
2
3
4
5
6
7
8
@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {

}

그리고 그 아래엔 @Component 데코레이터 안에, 이 컴포넌트에서 사용하는 다른 컴포넌트들을 components: 라는 값으로 나열해 줍니다.
이렇게 components 항목에 HelloWorld 컴포넌트를 추가해 주었기 때문에, 상단 템플릿 영역에서 <HelloWorld>와 같이 컴포넌트를 사용할 수 있게 됩니다.

1
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>

그렇다면 우리가 msg property로 넘긴 Welcom to Your Vue.js ~~~ 는 어떻게 HelloWorld 컴포넌트에서 불러올 수 있게 될까요?

/src/components/HelloWorld.vue 파일을 열어 봅시다.

line: 12 를 보면 msg라고 선언한 변수 앞에 @Prop() 이라는 데코레이터를 볼 수 있습니다. 바로 Prop이라는 데코레이터와 함께 변수를 선언 하면, 해당 변수 명의 prop을 바인딩 해서 받아올 수 있습니다.

1
2
3
4
5
Vue.component('HelloWorld', {
// props 정의
props: ['msg'],
template: '<div class="hello"><h1>{{ msg }}</h1></div>'
})

ES6 스타일에서는 위와 같은 코드라고 볼 수 있습니다.

다음 포스팅에서는 vue-property-decorator 에서 제공하는 데코레이터에 대해 좀 더 자세히 다뤄보도록 하겠습니다.

Comment and share

Typescript로 VueJS 프로젝트를 사용하는 방법을 알아봅니다.

Why Typescript?

왜 Typescript를 사용하느냐? 는 질문은, Typescript의 장점에 대해 구글링 해보면 많이 나오니, 두줄 정도로만 작성하겠습니다.

  • 자동완성을 통한 생산성 향상
  • 컴파일 레벨에서의 타입오류 검출로 인한 생산성 향상

물론, Typescript를 사용하면, 문법이 좀 더 장황해져서 타이핑이 늘어난다는 단점이 있겠으나, 위의 장점들이 이를 상쇄한다고 생각합니다.
Typescript를 사용하는 이유에 대해서는 훌륭한 글들이 많으니 참고하시면 좋을 것 같습니다.

사전 준비

pc 또는 mac에 nodejs와 npm이 설치 되어 있다는 전제 하에 진행합니다.

vue-cli 2 버전대에서는 Typescript를 지원하지 않았지만, 3버전대로 업그레이드 되면서, Typescript를 공식적으로 지원하기 시작했습니다.

기존에는 프로젝트 생성 후 수동으로 Typescript에 맞게 꾸며야만 했으나, vue cli 3 버전부터는 프로젝트 생성 시 옵션을 통하여 Typescript를 선택할 수 있어 많이 편해졌습니다.

터미널 (또는 CMD)창을 열어 아래 명령어를 입력하여 vue-cli를 설치 해 줍니다.

1
npm install -g @vue/cli

이제 간단히 vue cli 설치가 끝났습니다.

npm을 대신할 패키지 매니저 yarn도 설치 해줍니다.
(npm을 사용하실 분들은 설치하지 않으셔도 됩니다.)

1
npm install -g yarn

이제 콘솔에서 vue 명령어를 통해 vue cli를 호출할 수 있습니다.

1
vue --version

글 작성 시점의 최신 버전은 3.2.1이네요.

vue cli를 통해 프로젝트 생성

vue-cli를 통해 프로젝트를 생성하는것은 아주 쉽습니다.

터미널을 열고

1
vue ui

를 입력하여 브라우저를 통한 gui화면을 통해 뷰 프로젝트를 생성하고 관리할 수도 있습니다.

그러나, 터미널이 더 편한 개발자들도 많을 것 같습니다.
(속도도 더 빠르구요..)

1
vue create test-typescript-project

터미널에서 위 명령어를 입력합니다.

default(babel, eslint) 와 Manually select features 를 선택할 수 있습니다. 우리는 typescript를 사용할 것이기 때문에 Manually select features를 선택합니다.

선택한 후 프로젝트 구성에 맞는 옵션을 선택 합니다.

이번 샘플 프로젝트에서는 Babel, Typescript, Router, Vuex, CSS Pre-processors, Linter/Formatter만 선택하고 진행합니다.
(Unit Test나 E2E Testing은 제외합니다. 언제든 추후에 추가할 수 있습니다.)

babel은 js를 컴파일 하기 위한 녀석이라고 보면 되고, 두번째, Typescript가 핵심입니다.

Router는 라우팅 경로를 컨트롤하기 위한 vue 플러그인, Vuex는 상태관리를 위한 vue 플러그인이고,

CSS Pre-processors는 SCSS를 사용하기 위함, Linter/Formatter는 문법 검사 하는 녀석이라고 생각하면 됩니다. (더이상의 자세한 설명은 생략한다)

옵션들을 선택하고 Enter를 누르면, 아래와 같은 질문을 만나게 됩니다.

아래 첨부한 그림과 같이 선택해 줍니다.

모두 선택 한 후 또다시 Enter!!!

조금 기다린 후에, 아래와 같이 프로젝트가 완성 되었다는 메세지를 만날 수 있습니다.

터미널 하단에 성공적으로 프로젝트가 생성되었다는 메세지와 함께, 테스트 해볼 수 있는 명령어를 예시로 들어줍니다.

터미널에 그대로 입력해 봅니다.
로컬 개발 서버가 구동됩니다.

1
2
cd test-typescript-project
yarn serve

성공적으로 컴파일 되었다는 메세지와 함께
브라우저로 개발서버에 접속해 볼 수 있는 주소가 표시됩니다.

잘 작동되는것을 확인해 볼 수 있습니다.

다음 포스트에서는 대략적인 폴더 구조와 컴포넌트 구성을 해보겠습니다.
감사합니다.

Comment and share

Message broker

요즘 회사에서 새로운 시스템이 개발될 때, 레거시 시스템과의 연동이 많아지면서 Message broker 도입의 필요성에 대해 이야기 하고 있습니다. (조금 늦었지만..)

메세지 브로커 선택 시 어떤 브로커를 선택 하느냐 또한 중요한데요, 하자면 저희 회사에서는 Amazon MQ를 선택하게 되었습니다.

Amazon MQ는 지난 2018년7월부터 서울 리전에서도 사용 가능한 AWS 서비스 중 하나인데요, 내부적으로는 Apache Active MQ를 사용하도록 구성되어있습니다.

-Amazon MQ 장점

-구성이 쉽다 (유지보수 쉬움)

-가용성 (failover 지원)

-준수한 성능(22k건/초) (은 딱히 장점은 아닌듯 합니다)

-Amazon MQ 단점

-Rabbit MQ나 Apache Kafka에 비해서 상대적으로 떨어지는 인지도(레퍼런스가 덜 풍부함)

-Spring-cloud-stream 을 바로 사용할수 없음

서비스 선택 시 상세한 고려 사항은 https://stackshare.io/stackups/activemq-vs-kafka-vs-rabbitmq를 참고하셔도 됩니다.

Amazon MQ 선택시 마지막까지 고민하게 만들었던 부분은 spring-cloud-stream을 사용하여 구현할 수 없다는 점이었는데요, Spring-cloud-stream
을 사용하여 서비스를 구현하면 간단한 어노테이션 추가만으로도 구현이 가능했던 점이 아주 큰 장점이었거든요.

But, 찾아보니 Amazon MQ도 비슷한 난이도로 구현할 수 있는 방법이 있어 이를 구현하는 방법을 정리 차원에서 작성하고자 합니다. start!


Amazon console에서 Active MQ를 설정하는 방법은 건너뛰도록 하겠습니다.

  1. spring boot을 사용하여 Project를 생성

저는 spring-boot-devtools 정도만 추가 했습니다.(필요에 따라서는 lombok등도 추가 해주시면 됩니다..)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
  1. Maven pom.xml에 디펜던시 추가

activemq 관련 디펜던시를 추가해줍니다.

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
  1. application.properties에 설청값 셋팅
1
2
3
spring.activemq.broker-url=<broker-url>
spring.activemq.user=<user id>
spring.activemq.password=<password>
  1. 전달할 메세지 형태 클래스 Message.java를 추가 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.Serializable;

public class Message implements Serializable {

private static final long serialVersionUID = -1163890830946122942L;

private String id;
private String name;

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
  1. 메세지를 생성해서 MQ에 보낼 producer 클래스와 소비하게 될 consumer 클래스를 추가 합니다.

Consumer.java

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

@Component
public class Consumer {

@JmsListener(destination = "sample.queue")
public void receiveQueue(Message message) {
System.out.println(message.getId() + ", "+ message.getName());
}

}

Producer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.jms.Queue;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
public class Producer {

@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;

@Autowired
private Queue queue;

public void send(Message message) {
jmsMessagingTemplate.convertAndSend(queue, message);
}

}
  1. @SpringBootApplication클래스에 Bean 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class JmstestApplication {

@Bean
public Queue queue() {
return new ActiveMQQueue("sample.queue");
}

public static void main(String[] args) {
SpringApplication.run(JmstestApplication.class, args);
}
}


  1. 마지막으로 test class를 작성 해줍니다.

    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
    35
    36
    37
    38
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.rule.OutputCapture;
    import org.springframework.test.context.junit4.SpringRunner;

    import static org.assertj.core.api.Assertions.assertThat;

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class JmstestApplicationTests {

    @Rule
    public OutputCapture outputCapture = new OutputCapture();

    @Autowired
    private Producer producer;

    @Test
    public void sendSimpleMessage() throws InterruptedException {

    //given
    Message msg = new Message();
    msg.setId("1");
    msg.setName("a message from JH");

    //when
    producer.send(msg);


    //then
    Thread.sleep(1000L);
    assertThat(outputCapture.toString().contains("1, a message from JH")).isTrue();
    }

    }
  2. 테스트 클래스를 실행해봅시다. 실행 전에 실행 환경 변수에 SERIALIZABLE_PACKAGES 설정값을 추가 해 주어야
    오류 메세지를 만나지 않고 pojo를 전송하고 받을 수 있게 됩니다. 보안 상 신뢰할 수 있는 패키지를 환경설정값에 넣어야만 하도록 되어있기 때문입니다.

    1
    -Dorg.apache.activemq.SERIALIZABLE_PACKAGES="<패키지 경로>"
  3. 테스트 클래스 실행 결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    . ____ _ __ _ _
    /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
    \\/ ___)| |_)| | | | | || (_| | ) ) ) )
    ' |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot :: (v2.0.5.RELEASE)

    2018-10-14 16:33:24.224 INFO 54635 --- [ main] c..jmstest.JmstestApplicationTests : Starting JmstestApplicationTests on TF-Mac-023ui-MacBook-Pro.local with PID 54635 (started by jeonghunKim in /Users/tf-mac-023/IdeaProjects/jmstest)
    2018-10-14 16:33:24.226 INFO 54635 --- [ main] c..jmstest.JmstestApplicationTests : No active profile set, falling back to default profiles: default
    2018-10-14 16:33:24.387 INFO 54635 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6c1a5b54: startup date [Sun Oct 14 16:33:24 KST 2018]; root of context hierarchy
    2018-10-14 16:33:26.012 INFO 54635 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 2147483647
    2018-10-14 16:33:27.233 INFO 54635 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to ssl://
    2018-10-14 16:33:27.350 INFO 54635 --- [ main] c..jmstest.JmstestApplicationTests : Started JmstestApplicationTests in 3.996 seconds (JVM running for 6.586)
    2018-10-14 16:33:27.600 INFO 54635 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to ssl://
    1, a message from JH
    2018-10-14 16:33:28.961 INFO 54635 --- [ Thread-5] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6c1a5b54: startup date [Sun Oct 14 16:33:24 KST 2018]; root of context hierarchy
    2018-10-14 16:33:28.962 INFO 54635 --- [ Thread-5] o.s.c.support.DefaultLifecycleProcessor : Stopping beans in phase 2147483647

    Process finished with exit code 0

Comment and share

  • page 1 of 1

shockshot@naver.com

author.bio


author.job