(본 포스팅은 Vue 3와 타입스크립트를 사용하는 환경 기준으로 설명합니다)
안녕하세요
이번 포스팅에서는 Vue 3 버전부터 공식적으로 추가될 Composition API와
Vuex를 사용하는 방법에 대해 알아보려고 합니다.
기존에 사용하던 Vuex 저장소 접근 방식은 아래와 같이 바인딩된
this 컨텍스트의 $store를 통해 주입된 Vuex 저장소에 접근할 수 있었습니다.
// Vuex store
import vuex from 'vuex';
export default new Vuex.Store({
state: {
name: '근둥'
},
})
// Component
export default {
name: 'App',
created () {
console.log(this.$store.state.name); // 출력: 근둥
},
}
하지만, 컴포지션 API에는 this 바인딩이 존재하기 않기 때문에
기존처럼 Vuex 저장소에 직접적으로 접근할 수 없습니다.
(Vuex 뿐만 아니라 vue-router도 해당)
vuex(vue-router) 버전이 4.0.0 버전 미만인 경우, 데이터 제공(Provide)/주입(Inject)을 통해 해결할 수 있습니다.
(버전 4.0.0 이상은 게시물 최하단 참고)
상위 컴포넌트에서 특정 데이터를 제공하고,
하위의 컴포넌트에서 필요한 데이터를 주입받아 동일한 데이터를 공유할 수 있습니다.
상위 컴포넌트에서 Vuex 저장소를 제공하면,
하위 컴포넌트들이 Vuex 저장소에 접근하여 사용할 수 있다는 의미입니다.
화면에서 + 버튼을 누르면 카운트가 1증가하고, -버튼을 누르면 카운트가 1 감소하는
예제 코드를 살펴보며 직접 구현해봅시다.
먼저, 아래와 같은 Vuex 저장소를 생성했다고 가정합니다.
(src/store/index.ts)
import { createStore, Store } from 'vuex';
interface RootState {
count: number;
}
const initalState: RootState = {
count: 0,
};
const store = createStore({
state: initalState,
mutations: {
SET_COUNT(state, count) {
state.count = count;
},
},
actions: {
INCREASE({ state, commit }) {
commit('SET_COUNT', state.count + 1);
},
DECREASE({ state, commit }) {
commit('SET_COUNT', state.count - 1);
},
},
});
카운트(count) 상태를 가지고 있고, 카운트를 1씩 증가(INCREASE)/감소(DECREASE)시키는 액션과,
상태값을 변이시키는 SET_COUNT 뮤테이션으로 구성되어있습니다.
화면을 담당하는 컴포넌트는 아래와 같습니다.
(src/App.vue)
<template>
<div id="app">
<Home/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Home from '@/components/Home';
export default defineComponent({
name: 'App',
components: { Home },
setup() {
// TODO
},
});
</script>
(src/components/Home.vue)
<template>
<div class="home">
<h2>{{ count }}</h2>
<div>
<button @click="increase">+</button>
<button @click="decrease">-</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Home',
setup() {
const count = ???; // Vuex state: count
const increase = () => ???; // Vuex action: INCREASE
const decrease = () => ???; // Vuex action: DECREASE
return { count, increase, decrease };
},
});
</script>
위 컴포넌트에서 Vuex 저장소의 count 상태 값을 화면에 표시하고,
+ 버튼을 눌렀을 때 증가 액션을, - 버튼을 눌렀을 때 감소 액션을 호출하도록 구현하는 것이 우리의 목표입니다.
코드 구성을 확인해보면 아시겠지만, App.vue가 최상위 컴포넌트로 자리잡고 있습니다.
최상위 컴포넌트에서 Vuex 저장소를 제공(Provide)하면 하위의 모든 컴포넌트에서 주입(Inject)받아 사용할 수 있게 됩니다.
Vuex 저장소를 제공/주입할 수 있도록 아래와 같이 Vuex 코드를 수정합니다.
import { provide, inject } from 'vue';
import { createStore, Store } from 'vuex';
interface RootState {
count: number;
}
const initalState: RootState = {
count: 0,
};
const store = createStore({
state: initalState,
mutations: {
SET_COUNT(state, count) {
state.count = count;
},
},
actions: {
INCREASE({ state, commit }) {
commit('SET_COUNT', state.count + 1);
},
DECREASE({ state, commit }) {
commit('SET_COUNT', state.count - 1);
},
},
});
// Provide 구분 값
const StoreSymbol = Symbol();
// 저장소 제공 헬퍼 함수
export const provideStore = () => {
provide(StoreSymbol, store);
};
// 저장소 주입 헬퍼 함수
export const useStore = () => {
const store = inject<Store<RootState>>(StoreSymbol);
if (!store) {
throw new Error('No store provided');
}
return store;
};
export default store;
저장소 코드는 기존과 동일하며, 하단에 provideStore, useStore 헬퍼 함수가 추가로 구현되었습니다.
기존 Vue에서는 제공/주입 기능을 컴포넌트에서 사용할 수 있었지만,
제공/주입에 해당하는 컴포지션 API도 Vue 3에 함께 추가되었습니다.
컴포지션 API의 제공/주입은 provide와 inject 함수를 사용합니다.
먼저, Vuex 저장소를 제공하는 provideStore 헬퍼 함수는 아주 간단합니다.
생성한 Vuex 저장소 객체를 provide 함수를 통해 제공하도록 구현되어있는 것이 전부입니다.
provide 함수는 첫 번째 인자로 제공 대상에 대한 구분 값을 받으며, 두 번째 인자로는 제공할 데이터를 받습니다.
구분 값은 키 값이라고 볼 수 있으며, 이후 주입받을 때 해당 키 값에 해당하는 제공 데이터를 주입 받습니다.
useStore 헬퍼 함수는 반대로 제공하는 데이터를 주입받기 위해 inject 함수를 통해 데이터를 가져옵니다.
inject는 첫 번재 인자로 주입받을 대상의 구분 값을 받으며,
두 번째 인자로는 주입받을 대상이 없을 경우 사용할 기본 값을 받습니다 (Optional)
제공할 때의 구분 값을 StoreSymbol 값을 사용했기 때문에
주입할 때에도 동일한 값을 사용하여 Vuex 저장소 객체를 가져올 수 있습니다.
이렇게 구현한 헬퍼 함수는 컴포넌트에서 아래와 같이 사용할 수 있습니다.
(src/App.vue)
<template>
<div id="app">
<router-view />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { provideStore } from '@/store';
export default defineComponent({
name: 'App',
setup() {
provideStore(); // Vuex 저장소 제공
},
});
</script>
최상위 컴포넌트에서 Vuex 저장소를 제공하도록 provideStore 헬퍼 함수를 사용했습니다.
한 번 하위 컴포넌트에서 제공한 저장소를 주입받아 사용해봅시다.
(src/components/Home.vue)
<template>
<div class="home">
<h2>{{ count }}</h2>
<div>
<button @click="increase">+</button>
<button @click="decrease">-</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useStore } from '@/store';
export default defineComponent({
name: 'Home',
setup() {
const { state, dispatch } = useStore(); // 저장소 객체 주입받아 사용하기
const count = computed(() => state.count); // 저장소 count 상태 값
const increase = () => dispatch('INCREASE'); // INCREASE 액션 호출
const decrease = () => dispatch('DECREASE'); // DECREASE 액션 호출
return { count, increase, decrease };
},
});
</script>
useStore 헬퍼 함수를 통해 제공한 Vuex 저장소 객체를 주입받습니다.
Vue 2 버전의 this.$store와 동일하게 저장소의 상태와 액션, 변이 등에 접근하여 사용할 수 있습니다.
저장소의 count 상태를 반응형으로 화면에 렌더링 하기 위해 computed를 사용하고,
+, - 버튼이 눌렸을 때 카운트 상태를 변경하기 위해 INCREASE, DECREASE 액션을 디스패치 하도록 코드가 구현되었습니다.
기능을 테스트해보면, 아래와 같이 완벽하게 동작합니다!
본 포스팅에서는 Vuex 기준으로 작성했지만,
vue-router도 동일한 방법으로 제공/주입하도록 구현할 수 있습니다.
vuex 4 및 vue-router 4 with Composition API
현재 시점으로는 아직 베타버전 이지만, 위에서 구현했던 헬퍼 함수들을 제공합니다.
Vue.js 3 정식 릴리즈와 함께 버전 4들도 안정화되어 제공될 것이라고 생각합니다.
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
// setup
const store = useStore();
const route = useRoute();
const router = useRouter();
4 버전 부터는, 앞서 구현했던 것과 동일한 기능을 수행하는 헬퍼 함수를 자체적으로 제공합니다.
다만 위 방식을 사용하는 경우, 타입추론이 제대로 이루어지지 않는다는 단점이 존재합니다. (any)
import { useStore } from 'vuex';
const store = useStore(); // Store<any>
store.state. // 추론 불가...
위의 경우 선언한 Vuex 저장소 타입으로 직접 단언하여 타입을 명시하거나,
명확한 타입의 저장소/라우터를 제공하기 위해 직접 제공/주입하도록
구현할 수 있으니 적절히 사용하면 될 것 같습니다.
준비한 포스트는 여기까지이며,
다음에 더 다양한 내용으로 찾아뵙겠습니다.
감사합니다!
[참고 자료]
composition-api.vuejs.org/#plugin-development
v3.vuejs.org/guide/composition-api-provide-inject.html