본문 바로가기

프로그래밍(TA, AA)/자바스크립트

[Vue.js] Vuex를 이용한 상태 관리

컴포넌트는 정보의 전달을 위해서 props, event를 이용하게 된다. 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달하기 위해 props를 이용했고, 자식 컴포넌트에서 부모 컴포넌트로 정보를 전달하기 위해서 event를 사용했다.


한가지 주의할 점은 부모 컴포넌트로부터 전달받은 속성은 자식 컴포넌트에서 변경이 불가하다. 그렇기 때문에 자식 컴포넌트에서 부모 컴포넌트로부터 전달받은 속성을 직접 변경하는 것이 아니라 이벤트를 통해서 부모 컴포넌트에 변경 정보를 알려서 부모 컴포넌트 내부에서 변경하도록 해야한다. 부모-자식 관계가 아니거나 계층 구조가 복잡해지면 어쩔 수 없이 이벤트 버스(Event Bus) 객체를 사용해야 한다.


하지만 이벤트 버스 객체를 사용하는 방식도 대규모 애플리케이션을 개발하는 경우에는 복잡도를 증가시킬 수 밖에 없다. 이 때문에 대규모 애플리케이션의 상태를 관리할 수 있는 상태 관리 패턴이 필요한 것이다.


이를 위해 전역 객체를 생성하고 여러 개의 Vue 인스턴스나 컴포넌트가 특정 상태 혹은 데이터흐름을 공유할 수 있다. 그러나 공유 객체의 데이터는 여러 컴포넌트에 의해 함부로 변경될 수 있지만 추적이 불가능하게 된다. 이와 같은 경우 디버깅이 어렵다.


vuex는 이벤트 버스 객체를 사용하여 코드가 복잡해지는 문제점과 공유객체의 변경 추적이 힘들다는 문제점을 해결할 수 있는 좋은 패턴을 제공한다.




vuex는 vue.js 애플리케이션에서 상태관리 패턴을 지원하는 라이브러리다. 각 컴포넌트가 공유하는 상태 데이터는 전역에서 관리한다. 이와 같은 방식으로 부모에서 자식으로 또 그 자식으로 props를 이용해 속성을 계속해서 전달하지 않아도 되고, 상태 데이터를 변경하기 위해 부모 컴포넌트로 이벤트를 발생시키지 않아도 된다.


https://blog.logrocket.com/managing-multiple-central-stores-with-vuex-74cc44646043


Vuex는 저장소(store)를 중심으로 작동한다. 애플리케이션의 상태를 중앙집중화하여 관리하는 컨테이너이며 일반적인 전역 객체와는 달리 저장소의 상태를 직접 변경하지 않는다. 반드시 변이(mutation) 과정을 통해서만 변경이 가능하다.


변이는 기존 상태를 유지하고 새로운 값을 생성해낸다. 자바스크립트의 Array 객체의 filter 메서드처럼 새로운 상태를 만들어낸다. 그렇기 때문에 상태의 변화를 추적할 수 있으며 Time Travel Debugging이 가능하다.


한가지 주의할 점은 변이의 목적은 상태의 변화라는 점이다. 상태의 변화와 관련없는 작업이 변이 내부에서 수행되지 않도록 해야 한다. 또한 변이는 동기적인 작업이다. 비동기 처리는 변이를 통해서 수행할 수 없다.


상태변화와 관련이 없는 작업은 Action을 정의해서 Action을 통해 비즈니스 로직이 실행되게 하고, 그결과를 변이 쪽으로 Commit한다.


전체적인 흐름을 살펴보면 Vue 컴포넌트가 나타내는 UI는 저장소(Store)의 상태(State)를 반영하고, Vue 컴포넌트의 UI에서 발생하는 행위는 Action을 통해 외부 API를 호출한다. API 실행 결과는 변이를 통해서 다시 상태를 변화시킨다. 이렇듯 단방향 흐름으로 관리되어 상태변화의 추적이 용이하며 예측할 수 있는 방법으로 상태를 변경할 수가 있다.




[상태와 변이]

state와 mutation은 store 내부의 핵심 요소이다. state는 애플리케이션의 데이터이며, mutation은 상태를 변경하는 함수들을 보유하고 있는 객체다. 


state: 애플리케이션의 데이터

mutation: 상태를 변경하는 함수들의 집합 객체


전역에서 Vue.use(Vuex) 코드의 실행으로 애플리케이션 내부의 모든 컴포넌트가 저장소의 상태, 변이 객체에 접근할수 있게 된다. 상태는 반드시 변이를 통해서만 변경하도록 해야 한다. 그래야만 상태 변경 내역을 Vue devtools를 통해 추적할 수 있다.


어떤 애플리케이션 구조에서는 상태가 각각의 컴포넌트에 흩어져 있게 된다. 이 경우 상태 관리가 쉽지 않으며, 이벤트의 발신과 수신이 어지럽게 배치되어 코드가 복잡해지게 된다. 이때, Vuex를 통해 상태 관리를 집중화할수 있다.


npm install 명령어로 vuex를 설치한 후, 상태를 변화시키는 작업의 목록을 상수로 작성하여 저장소 객체를 추가한다. 변이를 일으킬때 문자열 정보를 전달하기 때문에 미리 상수로 정의해두면 편리하다.

export default {
	ADD_TODO: "addTodo",
	DONE_TOGGLE: "doneToggle",
	DELETE_TODO: "deleteTodo"
}
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../constant';
Vue.use(Vuex);

const store = new Vuex.Store({
	state: {
		todolist: [
			{ todo: "영화보기", done: false },
			{ todo: "주말산책", done: true },
			{ todo: "ES6 학습", done: false },
			{ todo: "잠실 야구장", done: false }
		]
	},
	mutations: {
		[Constant.ADD_TODO]: (state, payload) => {
			if (payload.todo !== "") {
				state.todolist.push({ todo: payload.todo, done: false });
			}
		},
		[Constant.DONE_TOGGLE]: (state, payload) => {
			state.todolist[payload.index].done = !state.todolist[payload.index].do
		},
		[Constant.DELETE_TODO]: (state, payload) => {
			state.todolist.splice(payload.index, 1);
		}
	}
})

export default store;


Vuex를 전역에서 사용할 수 있도록 Vue.use(Vuex) 코드를 미리 작성한다. 또한 state, mutation 정보를 전달하여 Vuex.Store 객체를 생성한다.


모든 컴포넌트의 상태 데이터를 Vuex로 관리할 필요는 없다. 하나의 컴포넌트 내부에서만 사용되는 상태를 Vuex 저장소에서 관리할 필요는 없다. Vuex가 유용한 경우는 여러 컴포넌트가 동일한 상태 데이터를 이용하는 경우다.


mutation 객체의 메서드들은 첫번째 인자가 상태(state)다. 두번째 인자 payload는 mutation에서 필요로하는 데이터이다. addTodo 작업의 경우는 todolist 데이터에 새로운 todo를 추가하기 때문에 payload를 통해서 todo를 전달하게 되는 방식이다. 만약 변이를 일으킬 때 필요한 인자가 여러개라면 payload를 객체 형태로 전달하면 된다.


두번째 단계 작업으로 src/main.js를 수정한다. vue 인스턴스를 생성할때 store 객체를 전달한다. 아래와 같은 코드를 통해 해당 Vue 애플리케이션의 모든 컴포넌트에서 Store 객체를 this.$store로 접근할 수 있게 된다.


import Vue from 'vue'
import store from './store'
import TodoList from './components/TodoList.vue'

new Vue({
	store,
	el: '#app',
	render: h => h(TodoList)
})
<style></style>
<template>
	<ul id="todolist">
		<li v-for="(a, index) in todolist" v-bind:class="checked(a.done)"
			v-on:click="doneToggle(index)">
			<span>{{ a.todo }}</span>
			<span v-if="a.done"> (완료)</span>
			<span class="close" v-on:click.stop="deleteTodo(index)">×</span>
		</li>
	</ul>
</template>
<script type="text/javascript">
import Constant from '../constant'

export default {
	computed: {
		todolist() {
			return this.$store.state.todolist;
		}
	},
	methods: {
		checked: function(done) {
			if(done) return { checked:true };
			else return { checked:false };
		},
		deleteTodo: function(index) {
			this.$store.commit(Constant.DELETE_TODO, {index: index});
		},
		doneToggle: function(index) {
			this.$store.commit(Constant.DONE_TOGGLE, {index: index});
		}
	}
}
</script>


위와 같이 List.vue 컴포넌트에서는 로컬 데이터가 없게된다. 이미 store가 Vue 인스턴스에 주입되었기 때문에 this.$store.tate로 접근할 수 있다. 이것을 속성처럼 접근할 수 있도록 하기 위해서 계산형 속성을 사용한다.


화면에서 일어나는 이벤트를 받아 처리하는 메서드에서 mutation을 일으키기 위해 this.$store.commit() 메서드를 호출한다. this.$store.commit() 메서드의 첫번째 인자는 변이의 이름이다. 변이에 전달할 인자는 payload 인자를 이용하면 된다.


<style></style>
<template>
	<div>
		<input class="input" type="text" id="task" v-model.trim="todo"
			placeholder="입력 후 엔터!" v-on:keyup.enter="addTodo">
		<span class="addbutton" v-on:click="addTodo">추가</span>
	</div>
</template>
<script type="text/javascript">
import Constant from '../constant'

export default {
	name: 'input-todo',
	data: function() {
		return { todo: "" }
	},
	methods: {
		addTodo: function() {
			this.$store.commit(Constant.ADD_TODO, { todo: this.todo });
			this.todo = "";
		}
	}
}
</script>

InputTodo.vue 컴포넌트는 로컬 상태 데이터를 가지고 있다. 왜냐하면 이 상태 데이터는 다른 컴포넌트에서는 이용되지 않기 때문이다. 이와 같이 해당 컴포넌트 안에서만 필요로하는 상태 데이터는 굳이 Vuex의 Store를 이용할 필요가 없다.


이 컴포넌트에서는 추가 버튼을 클릭하거나 엔터키를 누르면 새로운 데이터를 추가하기 위해 this.$store.commit()을 이용해 addTodo 변이를 일으킨다. todolist 상태 데이터가 변경되면 List.vue 컴포넌트는 반응형으로 작동해 화면을 갱신하게 된다.


계산형 속성으로 this.$store.sate를 직접 리턴하는 코드를 작성하고, 메서드에서도 mutation을 직접 commit하는 코드를 작성했으나 이러한 코드는 조금 불편해보인다. 이에 mapState, mapMutations와 같은 컴포넌트 바인딩 헬퍼 메서드를 제공하고 있다. 이밖에도 mapGetters, mapActions 메서드도 존재하나 우선 mapState, mapMutations 헬퍼 메서드를 이용해 코드를 변경해보겠다.

<template>
	<ul id="todolist">
		<li v-for="(a, index) in todolist" v-bind:class="checked(a.done)"
			v-on:click="doneToggle({index:index})">
			<span>{{ a.todo }}</span>
			<span v-if="a.done"> (완료)</span>
			<span class="close" v-on:click.stop="deleteTodo({index:index})">
			×</span>
		</li>
	</ul>
</template>
<script type="text/javascript">
import Constant from '../constant'
import { mapState, mapMutations } from 'vuex'
import _ from 'lodash';

export default {
	computed: mapState(['todolist']),
	methods: _.extend({
			checked(done) {
				if(done) return { checked:true };
				else return { checked:false };
			}
		},
		mapMutations([
			Constant.DELETE_TODO,
			Constant.DONE_TOGGLE
		])
	)
}
</script>


Vuex의 mapState, mapMutations 헬퍼 메서드를 참조한다. 그리고 mapState 코드를 이용해 todolist 상태 데이터를 List.vue 컴포넌트의 계산형 속성에 자동으로 바인딩한다. 전달하는 배열값에 상태의 이름을 나열하면 된다. 단 저장소 상태의 이름과 동일한 이름으로 바인딩되는것을 잊지 않아야 한다. 만일 다른 이름을 바인딩하고 싶다면 다음과 같이 계산형 속성을 수정할 수도 있다. todolist2로 계산형 속성명을 지정해보았다.

computed: mapState({
	todolist: (state) => state.todolist
}),


위 예제 전체에서는 methods 옵션을 작성할때 주의해야 한다. 변이를 일으키지 않는 메서드도 존재하기 때문이다. 이와 같은 경우 mapMutations 헬퍼 메서드가 만들어낸 객체와 변이를 일으키지 않는 메서드를 포함해서 객체를 병합할 필요가 있다. lodash 라이브러리의 _.extend() 메서드를 활용해 객체를 병합할 수 있다.


템플릿에서 메소드를 호출하는 구문도 변경해야 한다. 메서드에서 변이를 호출하는 형태가 동일해야 하기 때문이다. 만일 메소드 이름과 변이의 이름을 다르게 설정하고 싶다면 다음과 같이 설정할 수 있다.

methods: _.extend({
		checked(done) {
			if(done) return { checked: true };
			else return { checked: false };
		}
	},
	mapMutations({
		deleteTodo: Constant.DELETE_TODO,
		doneToggle: Constant.DONE_TOGGLE
	})
)


이제까지 상태와 변이에 대해 알아보았다. 변이는 상태를 변경하기 위해 존재하고, 동기적인 처리만 가능하다.





Getter는 간단하게 Store 수준의 Computed Property라고 생각하면 된다. 컴포넌트에서 계산형 속성이 필수가 아니듯이 Getter 또한 Store 내에서 필수적으로 사용해야 하는 것은 아니다. 하지만 적절하게 사용하면 컴포넌트에서의 코드 작성이 편리해진다.


우선 App.vue 내부에 배치할 컴포넌트를 둘로 나눈다. 버튼 리스트를 포함하는 RegionButtons.vue 컴포넌트, 국가 정보를 테이블에 보여주는 CountryList.vue 컴포넌트이다. 이 컴포넌트를 App.vue에서 참조하여 배치한다.

export default {
	CHANGE_REGION: "changeRegion"
}


이제 Store를 작성할 단계다. 이곳에 State, Mutation과 함께 Getter가 작성된다. src 디렉터리 아래 store 디렉토리를 생성한 후 index.js 파일을 작성한다.

import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
import _ from 'lodash';

Vue.use(Vuex);

const store = new Vuex.Store({
	state: {
		currentRegion: "all",
		countries: [
			{ no:1, name:"미국", capital:"워싱턴DC", region:"america" },
			...
			{ no:16, name:"서사모아", capital:"아피아", region:"oceania" },
		]
	},
	getters: {
		countriesByRegion(state) {
			if (state.currentRegion == "all") {
				return state.countries;
			} else {
				return state.countries.filter(c => c.region == state.currentRegion);
			}
		},
		region(state) {
			var temp = state.countries.map((c) => c.region);
			temp = _.uniq(temp);
			temp.splice(0, 0, "all");
			return temp;
		},
		currentRegion(state) {
			return state.currentRegion;
		}
	},
	mutations: {
		[Constant.CHANGE_REGION]: (state, payload) => {
			state.currentRegion = payload.region;
		}
	}
})

export default store;


상태 데이터는 현재의 지역(currentRegion)과 전체 국가 정보(countries)다. 하지만 우리가 화면에 보여줄 정보는 이 상태 데이터를 가공하여 만든 데이터(지역리스트, 지역별 국가정보, 현재 보여줄 지역명)인 것이다.


한가지 방법은 저장소에는 상태와 변이만 작성하고 각 컴포넌트에서 직접 계산형 속성과 메서드를 이용해 보여줄 데이터를 만들어낼 수 있다. 하지만 컴포넌트에서 작성해야할 코드가 많아지고, 동일한 데이터를 가공해야 할 여러 컴포넌트가 있다면 코드가 중복된다.


이 문제를 해결하기 위해 Getter를 이용할 수 있다. regions 게터는 배열의 map 메서드를 이용해 지역명만으로 이루어진 새로운 배열을 만들고 lodash 라이브러리의 _.uniq() 메소드를 이용해 중복 지역명을 제거한 지역명 리스트 정보를 만들어 낸다.


만약 지역리스트 정보만을 관리하는 별도의 state 데이터를 저장소에 추가하더라도 컴포넌트에서의 코드는 변경할 필요없이 Getter만 변경하면 된다. 이 저장소를 이용하는 각 컴포넌트를 작성해보겠다.

import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({
	store,
	el: '#app',
	render: h => h(App)
})
<template>
	<div id="app">
		<region-buttons />
		<country-list />
	</div>
</template>

<script>
import RegionButtons from './components/RegionButtons.vue';
import CountryList from './components/CountryList.vue';

export default {
	name: 'app',
	components: { RegionButtons, CountryList }
}
</script>

<style></style>


App.vue에서는 두 컴포넌트를 참조하고 템플릿에서 사용하도록 작성하면 된다. 이제 마지막으로 두개의 컴포넌트를 작성할 단계다.

<template>
	<div>
		<button class="region" v-for="region in regions"
			:class="isSelected(region)"
			@click="changeRegion({region:region})">
			{{region}}
		</button>
	</div>
</template>

<script>
import Constant from '../Constant'
import _ from 'lodash';
import { mapGetters, mapMutations } from 'vuex'

export default {
	name: "RegionButtons",
	computed: mapGetters([
		'regions', 'currentRegion'
	]),
	methods: _.extend(
		{
			isSelected(region) {
				if (region == this.currentRegion) return { selected: true };
				else return { selected: false }
			}
		},
		mapMutations([
			Constant.CHANGE_REGION
		])
	)
}
</script>


이 컴포넌트는 regions 게터를 이용해 버튼 리스트를 생성하고 버튼을 클릭하면 changeRegion 변이(mutation)를 일으킨다. mapGetters도 다른 헬퍼 메서드와 마찬가지로 다음과 같이 this.$store.getters와 같은 형태로 표현할 수 있다.

export default {
	name: "RegionButtons",
	......
	computed: {
		regions() {
			return this.$store.getters.regions;
		},
		currentRegion() {
			return this.$store.getters.currentRegion;
		}
	},
	......
}




앞에서 살펴본 변이(mutation)는 상태를 변경할 수 있지만 한가지 단점을 가지고 있다. 바로 동기적인 작업만 수행가능하다는 것이다. 예를 들어 서버와 통신하여 데이터를 가져온 후 상태 데이터를 변경하는 경우에 이 작업은 비동기적으로 처리되어야 하는데 변이는 동기적인 처리만 가능하므로 변이만으로는 문제 해결이 불가능하다.


그래서 변이의 기능에서 외부 리소스 액세스를 비롯한 API 호출 기능을 수행하기 위해 Action을 분리해낼 수밖에 없다. todolistapp을 액션을 사용하도록 변경해보겠다.

import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../constant';
Vue.use(Vuex);

const store = new Vuex.store({
	state: {
		......
	},
	mutations: {
		......
	},
	actions: {
		[Constant.ADD_TODO]: (store, payload) => {
			console.log("### addTodo!!!");
			store.commit(Constant.ADD_TODO, payload);
		},
		[Constant.DELETE_TODO]: (store, payload) => {
			console.log("### deleteTodo!!!");
			store.commit(Constant.DELETE_TODO, payload);
		},
		[Constant.DONE_TOGGLE]: (store, payload) => {
			console.log("### doneToggle!!!");
			store.commit(Constant.DONE_TOGGLE, payload);
		}
	}
})

export default store;


액션의 메서드들은 첫번째 인자로 store 객체가 전달되고, 두번째 인자가 payload다. 액션에서 mutations을 일으키고 싶다면 store 객체의 commit 메서드를 이용하면 된다.


전달된 store 객체를 통해서 state, getters, mutations 모두 이용할 수 있다. 위 코드는 간단한 예제이지만 상당히 복잡한 비즈니스 로직도 배치할 수 있다.

actions : {
	[Constant.ADD_TODO]: ({ state, commit }, payload) => {
		console.log("###addTodo!!!");
		commit(Constant.ADD_TODO, payload);
	},
	......
}


이제 컴포넌트에서 액션을 호출하도록 List.vue와 InputTodo.vue를 변경한다. 나머지 부분은 그대로 두고 methods 부분만 변경하면 된다.

import Constant from '../constant'

export default {
	......
	methods : {
		addTodo: function() {
			this.$state.dispatch(Constant.ADD_TODO, { todo: this.todo });
			this.todo = "";
		}
	}
}
import Constant from '../constant'
import { mapState } from 'vuex'
import _ from 'lodash';

export default {
	computed: mapState(['todolist']),
	methods: {
		checked: function(done) {
			if(done) return { checked: true };
			else return { checked: false };
		},
		deleteTodo: function(payload) {
			this.$store.dispatch(Constant.DELETE_TODO, payload);
		},
		doneToggle: function(payload) {
			this.$store.dispatch(Constant.DONE_TOGGLE, payload);
		}
	}
}


this.$store.dispatch() 메서드를 이용해 액션을 호출한다. 만일 메서드가 동일한 이름의 액션을 호출한다면 다음과 같이 mapActions 헬퍼 메서드를 이용할 수 있다.

export default {
	computed: mapState(['todolist']),
	methods: _.extend({
			checked: function(done) {
				if(done) return { checked: true };
				else return { checked: false };
			}
		},
		mapActions([
			Constant.DELETE_TODO, Constant.DONE_TOGGLE
		])
	)
}


액션을 사용하는 목적 중 하나는 비동기 처리다. 이번 예제는 비동기 처리를 수행하는 예제다 Vuex와 axios를 사용하도록 변경하고 서버로 검색 요청하는 기능을 비동기 액션 처리로 구현한다. 

export default {
	SEARCH_CANTACT: "searchContact",
	BASE_URL: "http://localhost:8080/contacts/search/"
}
import axios from 'axios';
import Constant from '../constant';

export default {
	searchContact: (name) => {
		return axios.get(Constant.BASE_URL + name);
	}
}

axios를 호출하여 리턴된 Promise 객체를 그대로 리턴한다. 액션에서는 이 Promise 객체를 리턴받아 .then() 메서드로 비동기 처리하여 mutation을 commit하게 된다.

import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../constant';
import SearchApi from '../api/SearchApi';

Vue.use(Vuex);

const store = new Vuex.Store({
	state: {
		contacts: []
	},
	mutations: {
		[Constant.SEARCH_CONTACT]: (state, payload) => {
			state.contacts = payload.contacts;
		}
	},
	actions: {
		[Constant.SEARCH_CONTACT]: (store, payload) => {
			SearchApi.searchContact(payload.name)
			.then((response) => {
				store.commit(Constant.SEARCH_CONTACT, {contacts: response.data})
			})
		}
	}
})

export default store;

변이는 상태를 변경하는데 집중하고 api를 호출하는 등의 작업은 액션에서 담당한다. API의 searchContact 메서드를 호출한 결과인 Promise 객체를 리턴받아 비동기를 처리하는 then() 메서드를 작성했다. then() 내부에서 데이터를 수신한 후에 변이를 일으키기 위해 commit() 메서드를 호출한다. 바로 비동기 작업의 수행 결과를 이용해 상태를 변경한 것이다.


Promise 패턴대신 async~await를 사용해도 된다. async~await를 사용하기 위해서는 몇가지 의존 라이브러리를 추가하고 설정 파일도 변경해야 한다. 우선 npm install 명령어로 의존 라이브러리를 다운로드 한다.


npm install --save babel-preset-stage-2 babel-plugin-transform-runtime


ContactList.vue, Serach.vue 컴포넌트를 작성하겠다. 이 컴포넌트들이 저장장소의 상태와 액션을 이용한다.

<template>
	<div>
		<table id="list">
			<thead>
				<tr>
					<th>번호</th>
					<th>이름</th>
					<th>전화번호</th>
					<th>주소</th>
				</tr>
			</thead>
			<tbody id="contacts">
				<tr v-for="contact in contacts">
					<td>{{contact.no}}</td>
					<td>{{contact.name}}</td>
					<td>{{contact.tel}}</td>
					<td>{{contact.address}}</td>
				</tr>
			</tbody>
		</table>
	</div>
</template>

<script type="text/javascript">
import { mapState } from 'vuex';

export default {
	name: "contact-list",
	computed: mapState([ 'contacts' ])
}
</script>
<template>
	<p>
		이름: <input tye="text" v-model.trim="name"
			placeholder="두글자 이상 입력후 엔터!"
			@keyup.enter="keyupEvent" />
	</p>
</template>
<script type="text/javascript">
import Constant from '../constant';

export default {
	name: 'search',
	data: function() {
		return { name: '' };
	},
	methods: {
		keyEvent: function(e) {
			var val = e.target.value;
			if (val.length >= 2) {
				this.$store.dispatch(Constant.SEARCH_CONTACT, { name: val })
				this.name = "";
			} else {
				this.$store.dispatch(Constant.SEARCH_CONTACT, { name: '' })
			}
		}
	}
}
</script>


저장소의 상태 데이터를 ContactList.vue 컴포넌트의 계산형 속성에 바인딩한다. 검색할 이름을 입력하고 엔터키를 누르면 검색어를 이용해 액션을 호출하기 위해 dispatch() 메서드를 이용한다.


store 객체를 Vue 인스턴스에 추가하고 IE에서도 원활하게 실행될 수 있도록 es6-promise를 참조하여 polyfill() 메서드를 호출한다.


IE는 ES6의 promise 객체를 지원하지 않는다. IE에서 promise 객체를 사용해 비동기 작업을 수행하려면 es6-promise 패키지를 npm으로 다운로드하고 사용해야 한다.




대규모 애플리케이션을 개발할때는 store를 하나의 파일로 관리하기가 쉽지 않다. 이런 경우에는 여러개의 파일로 분리해서 관리할 수 있는 방법이 필요하다.


역할별 분리: 상태, 변이, 액션, 게터의 역할 단위로 파일 분리

모듈별 분리: 여러개의 모듈로 나누어 관리


모듈은 자체적인 상태, 변이, 액션, 게터를 가지는 store의 하위 객체이다. 주 저장소에서는 여러 개의 모듈들을 포함할 수 있다. 모듈 객체를 만들때 각각의 상태와 변이, 액션을 정의하고 저장소 객체를 만들때 modules 속성으로 지정하면 된다.

const m1 = {
	state: { ... },
	mutations: { ... },
	actions: { ... },
	getters: { ... }
}

const m2 = {
	state: { ... },
	mutations: { ... },
	actions: { ... }
}

const store = new Vuex.Store({
	...
	modules: {
		a: m1,
		b: m2
	}
})

store와 모듈은 각각의 state, mutation, getter, action을 가진다. 모든 모듈, 저장소에서 action과 mutations은 공유하게되지만 state와 getter는 공유되지 않는다. 간혹 모듈의 액션에서 상위 저장소의 상태를 이용해야 하는 경우가 있을 수 있다. 이때 모듈의 액션에서는 첫번째 인자인 store 객체를 통해서 저장소의 Root State에 접근할 수 있다. 모듈의 액션에서의 store 객체 속성은 다음과 같다.


 속성명

 설명

 commit

 변이를 일으키기 위한 메서드

 dispatch

 액션을 호출하기 위한 메서드. 한 액션에서 다른 액션 호출 가능

 getters

 모듈 자기 자신의 게터

 rootGetters

 루트 저장소의 게터

 state

 모듈 자기 자신의 상태 데이터

 rootState

 루트 저장소의 상태데이터


루트 저장소의 액션에서는 모듈의 상태에 접근할 수가 없다. 그렇기 때문에 상태 데이터가 전역 수준에서 이용되는지, 특정 모듈에서만 이용되는지를 구분해야 한다.


import Constant from '../constant';
import SearchApi from '../api/SearchApi';

export default {
	state: {
		contacts: []
	},
	mutations: {
		[Constant.SEARCH_CONTACT]: (state, payload) => {
			state.contacts = payload.contacts;
		}
	},
	actions: {
		[Constant.SEARCH_CONTACT] : (store, payload) => {
			SearchApi.searchContact(payload.name)
			.then((response) => {
				store.commit(Constant.SEARCH_CONTACT, { contacts: response.data })
				if (response.data.length > 0)
					store.dispatch(Constant.ADD_KEYWORD, payload);
			})
		}
	}
}
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../constant';
import module1 from './module1';

Vue.use(Vuex);

const store = new Vuex.Store({
	state: {
		keywordlist: []
	},
	mutations: {
		[Constant.ADD_KEYWORD]: (state, payload) => {
			state.keywordlist.splice(0, 0, payload.name);
		}
	},
	actions: {
		[Constant.ADD_KEYWORD]: (store, payload) => {
			store.commit(Constant.ADD_KEYWORD, payload);
		}
	},
	modules: { m1 : module1 }
})

export default store;


주목해서 살펴볼 부분은 모듈의 액션 기능이다. 연락처 검색이 성공적으로 완료되면 store.commit() 메서드를 이용해 상태를 변경시킨다. 검색된 연락처가 존재하는 경우에만 검색명을 루트 저장소의 keywordlist 상태 데이터에 추가하도록 addKeyword 액션을 디스패치한다. 이와 같이 액션에서 다른 모듈의 액션, 변이를 이용하도록 작성할 수 있으며, 루트 상태를 활용할 수도 있다.