왜 Vuex를 사용하는가?
- MVVM패턴에서는 데이터가 핵심. 데이터가 변경되면 View Model을 통해서 View가 즉시 변경됨
- 모든 화면은 데이터에의해 결정되는 구조이기 때문에 애플리케이션의 데이터를 체계적으로 구조화 하는 것이 중요.
- eventBus의 문제점
- 자식 컴포넌트들의 계층구조가 복잡해지면 일일히 부모 컴포넌트에 저장된 정보를 계층 구조를 따라 속성으로 전달해야 함
- 특히 유지 보수 중에 새로운 상태 정보가 추가되면(data 옵션 객체에 정보가 추가되면) 최종 자식 컴포넌트까지 전달되는 경로에 모든 컴포넌트들의 속성(props) 옵션이 모두 변경되어야 함.
- 전역객체의 문제점
- 상태가 어느 컴포넌트, 어떤 메서드에 의해서 언데, 어떻게 변경하는지 추적하기 힘듦.(데이터를 변경하는 것은 모든 컴포넌트에서 하기때문)
- Vuex 와 같은 상태관리 라이브러리가 필요
- 중앙 집중화된 상태 정보 관리가 필요하다.
- 상태정보가 변경되는 상황과 시간을 추적하고 싶다.
- 컴포넌트에서 상태정보를 안전하게 접근하고 싶다.
- 하지만 간단한 애플리케이션이라면 EventBus 객체의 사용정도여도 충분히 해결가능
Vuex란?
Vue 아키텍처 - 단방향 데이터 흐름
- 컴포넌트가 Action을 일으킴 (예) 버튼클릭)
- Action 에서는 외부 API를 호출한 뒤 그 결과를 이용해 Mutation(변이)을 일으킴( 외부 API 가 없으면 생략 )
- Mutation에서는 Action의 결과를 받아 State(상태)를 변경. 이 단계에서는 추적할 수 있기 때문에 Vue.js DevTools 와 같은 도구를 이용하면 상태 변경 내역을 모두 확인 가능.
- Mutation에 의해 변경된 상태는 다시 컴포넌트에 바인딩되어 화면에 갱신됨
점선으로 표시된 부분이 Vuex 저장소 객체 영역
- 저장소가 상태, 변이, 액션 모두 관리
- 저장소는 애플리케이션의 상태를 중앙집중화하여 관리하는 컨테이너이며 일반적인 전역 객체와는 달리 저장소의 상태를 직접 변경하지 않음. 저장소의 상태는 반드시 변이를 통해서만 변경
주의할 점
- 상태와 관련없는 작업에 변이에 수행되지 않도록 함
- 변이의 목적은 상태의 변경.
- 비동기 처리는 변이를 통해서 수행하지 않음.
- 사후 스냅샷을 캡처한 후 상태가 변경되기 때문에 변이에의한 데이터 추적이 불가능해짐
- 상태와 관련없는 작업은 액션을 정의해서 실행하여 비즈니스 로직이 실행 되도록 함
- 액션의 처리결과는 변이를 호출할 때 전달하여 상태를 변경함
상태와 변이
- yarn add vuex 또는 npm install --save vuex 로 설치
- npm install 로 추가하는 경우 오류가 있을 수 있는데 node_modules 디렉터리를 삭제하고 명령어를 재실행하면 된다.
C:\JetBrains\vscode_workspace\totolistapp>yarn add vuex
yarn add v1.22.10
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "win32" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "win32" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ vuex@3.5.1
info All dependencies
└─ vuex@3.5.1
Done in 12.50s.
C:\JetBrains\vscode_workspace\totolistapp>
src/Constant.js
- 오타를 줄일 수 있음
- 해당 파일만 살펴보면 어떤 변이나 액션이 일어나는지 한눈에 볼 수 있음
export default {
ADD_TODO: 'addTodo',
DONE_TOGGLE: 'doneToggle',
DELETE_TODO: 'deleteTodo'
}
src/store/index.js
- 모든 컴포넌트의 상태 데이터를 Vuex로 관리할 필요는 없다.
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
/*
모든 자식 컴포넌트에서 저장소 객체를
this.$store로 접근 가능하게 함
*/
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
todolist: [
{ id: 1, todo: "영화보기", done: false },
{ id: 2, todo: "주말 산책", done: true },
{ id: 3, todo: "ES6 학습", done: false },
{ id: 4, todo: "잠실 야구장", done: false },
]
},
mutations: {
/*
state : 상태
payload : 변이를 필요로 하는 데이터
- 변이를 일으킬 때 필요한 인자가 여러 개라면 객체형태로 전달
*/
[Constant.ADD_TODO]: (state, payload) => {
if (payload.todo === "") return;
state.todolist.push({ id: new Date().getTime(), todo: payload.todo, done: false });
},
[Constant.DONE_TOGGLE]: (state, payload) => {
var index = state.todolist.findIndex((item) => item.id === payload.id);
state.todolist[index].done = !state.todolist[index].done;
},
[Constant.DELETE_TODO]: (state, payload) => {
var index = state.todolist.findIndex((item) => item.id === payload.id);
state.todolist.splice(index, 1);
},
}
});
export default store;
src/main.js
import Vue from 'vue'
import TodoList from './components/TodoList.vue'
import store from './store';
Vue.config.productionTip = false
new Vue({
/*
Vue.use(Vuex) 와 더불어
모든 자식 컴포넌트에서 저장소 객체를
this.$store로 접근 가능하게 함
*/
store,
render: h => h(TodoList),
}).$mount('#app')
src/components/List.vue
- 로컬 데이터가 없음
<template>
....
</template>
<script type="text/javascript">
import Constant from '../Constant';
export default {
name : 'List',
/*
계산형 속성을 사용했다는 것은
읽기 전용으로 사용하겠다는 의미
*/
computed : {
todolist() {
return this.$store.state.todolist;
}
},
methods : {
checked: function(done){
if(done) return {checked: true};
else return {checked: false};
},
doneToggle : function(id) {
/*
첫번째 인자 : 변이의 이름
두번째 인자 : payload 인자
*/
this.$store.commit(Constant.DONE_TOGGLE, {id: id});
},
deleteTodo : function(id) {
this.$store.commit(Constant.DELETE_TODO, {id: id});
}
}
}
</script>
src/components/InputTodo.vue
- 로컬상태 데이터를 가지고 있음
- 이 상태 데이터는 다른 컴포넌트에서는 이용되지 않으며 관리해야할 만큼 중요한 데이터가 아님
- 특정 컴포넌트에서만 필요로 하는 데이터는 굳이 Vuex의 저장소를 이용할 필요가 없음
<template>
....
</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>
Vue devTools
- Base State를 기점으로 변이가 일어나서 변경되는 상태를 모두 기록
- 시계모양 아이콘을 클릭 시 다시 이전 상태로 돌아가서 기능 테스트가 가능(시간여행 디버깅)
헬퍼 메서드
mapState
- 계산형 속성을 직접 작성하지 않아도 됨
- 저장소의 상태의 이름과 동일한 이름으로 바인딩
import {mapState, mapMutations} from 'vuex';
export default {
....
computed : mapState(['todolist']),
....
}
- 다른 이름으로 바인딩 하고 싶으면 다음과 같이 작성 ( 여기서는 todolist2 로 계산형 속성명 지정 )
computed : mapState({
todolist2 : (state) => state.todolist
})
mapMutation
- 변이를 동일한 이름의 메서드에 자동으로 연결
- 변이를 일으키지 않는 메서드가 함께 존재할 때는 병합하기 위해 ES6의 전개연산자를 이용
- 주의할 점
- 변이 메서드에 바인딩 할 경우 변이의 인자형식을 따름 ( {id: a.id} )
<template>
<ul id="todolist">
<li v-for="a in todolist" :key="a.id" :class="checked(a.done)"
@click="doneToggle({id: a.id})">
.....
<span class="close" @click.stop="deleteTodo({id:a.id})">×</span>
</li>
</ul>
</template>
<script>
import {mapState, mapMutations} from 'vuex';
export default {
....
methods : {
checked: function(done){
if(done) return {checked: true};
else return {checked: false};
},
...mapMutations({
deleteTodo : Constant.DELETE_TODO,
doneToggle : Constant.DONE_TOGGLE
})
}
....
}
</script>
컴포넌트 수준에서 직접 상태 변경 막기
- strict: true 옵션은 컴포넌트에서 직접 상태를 변경하려 할 경우 오류를 발생시킴
- 엄격한 검증으로 성능이 저하 될 수 있기 때문에 운영환경 배포 시에는 false를 권장
- 계산형 속성으로 읽기전용을 만들어 직접 상태 변경을 막는 것이 좋음
const store = new Vuex.Store({
state,
mutations,
actions,
strict : true
});
게터
- 상태 데이터는 현재의 지역(currentRegion)과 전체 국가 정보(countries)임
- 우리가 화면에 보여줄 정보는 이 상태 데이터를 가공하여 만든 데이터이며, 바로 지역 리스트(regions), 지역별 국가정보(countriesByRegion), 현재 보여줄 지역명
- 한가지 방법은 저장소에는 상태와 변이만 작성하고 각 컴포넌트에서 직접 계산형 속성과 메서드를 이용해 보여줄 데이터를 만들어 낼 수 있음.
- 하지만 컴포넌트에서 작성해야 할 코드가 많아지고, 동일한 데이터를 가공해야 할 여러 컴포넌트가 있다면 코드가 중복이 됨
- 문제를 해결하기 위해 게터를 이용할 수 있음
- gettters 를 살펴보면 앞의 지역 리스트, 지역별 국가정보, 현재 보여줄 지역명 정보를 게터로 작성한 것을 볼 수 있습니다.
- 지역명 정보를 다르게 보여준다면, 게터 메소드만 변경하고 각 컴포넌트는 코드를 변경할 필요가 없음
저장소(store) 수준의 계산형 속성
vue create countryapp
cd countryapp
yarn add vuex loadash
-- 또는 --
npm install --save vuex loadash
src/Constant.js
export default {
CHANGE_REGION : 'changeRegion'
}
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: 2, name: "프랑스", capital: "파리", region: "europe" },
{ no: 3, name: "영국", capital: "런던", region: "europe" },
{ no: 4, name: "중국", capital: "베이징", region: "asia" },
{ no: 5, name: "태국", capital: "방콕", region: "asia" },
{ no: 6, name: "모로코", capital: "라바트", region: "africa" },
{ no: 7, name: "라오스", capital: "비엔티안", region: "asia" },
{ no: 8, name: "베트남", capital: "하노이", region: "asia" },
{ no: 9, name: "피지", capital: "수바", region: "ocearnia" },
{ no: 10, name: "솔로몬 제도", capital: "호니아라", region: "ocearnia" },
{ no: 11, name: "자메이카", capital: "킹스턴", region: "america" },
{ no: 12, name: "나미비아", capital: "빈트후크", region: "africa" },
{ no: 13, name: "동티모르", capital: "딜리", region: "asia" },
{ no: 14, name: "멕시코", capital: "멕시코시티", region: "america" },
{ no: 15, name: "베네수엘라", capital: "키라시스", region: "america" },
{ no: 16, name: "서사모아", capital: "아피아", region: "ocearnia" }
]
},
getters: {
countriesByRegion(state) {
if (state.currentRegion == 'all') {
return state.countries;
} else {
return state.countries.filter((c) => c.region == state.currentRegion);
}
},
regions(state) {
var temp = state.countries.map((c) => c.region);
// _.uniq 함수로 지역명 중복 제거
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;
src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store';
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
src/App.vue
<template>
<div id="app">
<region-buttons></region-buttons>
<country-list></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>
src/components/RegionButtons.vue
<template>
<div>
<button class="region"
v-for="region in regions" :key="region"
:class="isSelected(region)"
@click="changeRegion({region:region})">{{region}}</button>
</div>
</template>
<script>
import Constant from '../Constant';
import {mapMutations} from 'vuex';
export default {
name : "Regions",
computed : {
regions: function() {
return this.$store.getters.regions;
},
currentRegion() {
return this.$store.getters.currentRegion;
}
},
/*
헬퍼메서드로 간결하게 넣을 수 있음
computed: mapGetters(['regions','currentRegion']);
*/
methods: {
isSelected(region) {
if(region == this.currentRegion) return {selected : true};
else return {selected : false};
},
...mapMutations([
Constant.CHANGE_REGION
])
}
}
</script>
<style scoped>
button.region { text-align: center; width: 80px; margin: 2px; border:solid 1px gray;}
button.selected { background-color: purple; color:aqua;}
</style>
src/components/CountryList.vue
<template>
<div id="example">
<table id="list">
<thead>
<tr>
<th>번호</th>
<th>국가명</th>
<th>수도</th>
<th>지역</th>
</tr>
</thead>
<tbody id="contacts">
<tr v-for="c in countries" :key="c.no">
<td>{{c.no}}</td>
<td>{{c.name}}</td>
<td>{{c.capital}}</td>
<td>{{c.region}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name : 'CountryList',
computed : mapGetters({
countries : 'countriesByRegion'
})
}
</script>
<style scoped>
#list { width: 520px; border: 1px solid black; border-collapse: collapse;}
#list td, #list th { border: 1px solid black; text-align: center;}
#list td:nth-child(4n+1), #list th:nth-child(4n+1) { width: 100px;}
#list td:nth-child(4n+2), #list th:nth-child(4n+2) { width: 150px;}
#list td:nth-child(4n+3), #list th:nth-child(4n+3) { width: 150px;}
#list td:nth-child(4n), #list th:nth-child(4n) { width: 120px;}
#list > thead > tr { color: yellow; background-color: purple;}
</style>
액션
액션 이용하기
- 변이의 단점: 상태를 변경할 수 있지만 동기적인 작업만 가능하다.
- 비동기 처리가 필요한 API기능을 수행하기 위해 Action을 분리하여 구현
totolistapp 로 프로젝트 변경
src/store/index.js 액션 변경
const store = new Vuex.Store({
state: {
....
},
mutations: {
....
},
actions: {
// 구조 분해 할당을 통해 코딩일 더욱 간편히 할 수 있음
[Constant.ADD_TODO]: ({ store, commit }, payload) => {
console.log("###addTodo!!!", payload);
commit(Constant.ADD_TODO, payload);
},
[Constant.DELETE_TODO]: (store, payload) => {
console.log("###deleteTodo!!!", payload);
store.commit(Constant.DELETE_TODO, payload);
},
[Constant.DONE_TOGGLE]: (store, payload) => {
console.log("###doneToggle!!!", payload);
store.commit(Constant.DONE_TOGGLE, payload);
}
}
});
export default store;
src/components/InputTodo.vue 변경
<script type="text/javascript">
import Constant from '../Constant';
export default {
name : 'input-todo',
data : function(){ return { todo: ""} },
methods : {
addTodo : function() {
this.$store.dispatch(Constant.ADD_TODO, {todo: this.todo});
this.todo = "";
}
}
}
</script>
src/components/List.vue 변경
<script type="text/javascript">
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
name : 'List',
computed : mapState(['todolist']),
methods : {
....
doneToggle : function(payload) {
this.$store.dispatch(Constant.DONE_TOGGLE, payload);
},
deleteTodo : function(payload) {
this.$store.dispatch(Constant.DELETE_TODO, payload);
}
}
}
</script>
mapActions
- 메서드가 동일한 이름의 액션을 호출한다면 mapActions 헬퍼 메서드를 이용할 수 있음
<script type="text/javascript">
import Constant from '../Constant';
import {mapState, mapActions} from 'vuex';
export default {
name : 'List',
computed : mapState(['todolist']),
methods : {
checked: function(done){
if(done) return {checked: true};
else return {checked: false};
},
...mapActions({
deleteTodo : Constant.DELETE_TODO,
doneToggle : Constant.DONE_TOGGLE
})
}
}
</script>
액션을 이용한 비동기 처리
vue create contactapp_search
cd contactapp_search
yarn add axios vuex es6-promise
또는
npm install --save axios vuex es6-promise
src/Constant.js
export default {
SEARCH_CONTACT : "searchContact",
BASE_URL : "http://sample.bmaster.kro.kr/contacts_long/search/"
}
src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
import axios from 'axios';
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) => {
axios.get(Constant.BASE_URL + payload.name).then((response) => {
store.commit(Constant.SEARCH_CONTACT, { contacts: response.data });
})
}
}
});
export default store;
src/App.vue
<template>
<div>
<search placeholder="두글자 이상 입력 후 엔터!"></search>
<contact-list></contact-list>
</div>
</template>
<script>
import ContactList from './components/ContactList.vue'
import Search from './components/Search.vue'
export default {
name: 'app',
components: {
ContactList,Search
}
}
</script>
src/components/ContactList.vue
<style scoped>
#list { width:600px; border: 1px solid black; border-collapse: collapse;}
#list td, #list th { border: 1px solid black; text-align: center;}
#list > thead > tr { color: yellow; background-color: purple;}
</style>
<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" :key="contact.no">
<td>{{contact.no}}</td>
<td>{{contact.name}}</td>
<td>{{contact.tel}}</td>
<td>{{contact.address}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name : 'contact-list',
computed: mapState(['contacts'])
}
</script>
src/components/Search.vue
<template>
<p>
이름 :<input type="text" placeholder="두글자 이상 입력후 엔터!!"
v-model.trim="name" @keyup.enter="keyupEvent">
</p>
</template>
<script>
import Constant from '../Constant';
export default {
name : 'search',
data() {
return { name: ''};
},
methods: {
keyupEvent : 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>
src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import ES6Promise from 'es6-promise'
ES6Promise.polyfill();
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
액션의 기능
액션은 인자로 저장소(store) 와 페이로드(payload)를 전달하고 있어서 저장소의 객체의 상태, 변이, 게터, 엑션 4가지 모두를 이용가능
- 기존의 상태 정보를 이용해 액션 수행가능
- 액션에서 여러 변이 commit 가능
- 액션에서 다른 액션 dispatch 가능
다음과 같이 은행 계좌 애플리케이션에 입금, 인출 기능이 있음
계좌 이체 기능을 추가
- 이체 기능 : 인출 -> 입급 기존 액션을 이용하는 것이 효율적
- 액션들을 여러개 순차적으로 연결하면 복잡한 비즈니스 로직을 쉽게 구현
대규모 애플리케이션에서의 Vuex 사용
대규모 애플리케이션에는 store 파일을 하나의 파일롸 관리하기 어렵기 때문에 여러개의 파일로 분리가 필요
역할별 분리
totolistapp 프로젝트를 상태, 변이, 액션으로 구분 분리
src/store/state.js
export default {
todolist: [
{ id: 1, todo: "영화보기", done: false },
{ id: 2, todo: "주말 산책", done: true },
{ id: 3, todo: "ES6 학습", done: false },
{ id: 4, todo: "잠실 야구장", done: false }
]
}
src/store/mutations.js
import Constant from '../Constant';
export default {
[Constant.ADD_TODO]: (state, payload) => {
if (payload.todo === "") return;
state.todolist.push({ id: new Date().getTime(), todo: payload.todo, done: false });
},
[Constant.DONE_TOGGLE]: (state, payload) => {
var index = state.todolist.findIndex((item) => item.id === payload.id);
state.todolist[index].done = !state.todolist[index].done;
},
[Constant.DELETE_TODO]: (state, payload) => {
var index = state.todolist.findIndex((item) => item.id === payload.id);
state.todolist.splice(index, 1);
}
}
src/store/actions.js
import Constant from '../Constant';
export default {
// 구조 분해 할당을 통해 코딩일 더욱 간편히 할 수 있음
[Constant.ADD_TODO]: ({ store, commit }, payload) => {
console.log("###addTodo!!!", store, payload);
commit(Constant.ADD_TODO, payload);
},
[Constant.DELETE_TODO]: (store, payload) => {
console.log("###deleteTodo!!!", payload);
store.commit(Constant.DELETE_TODO, payload);
},
[Constant.DONE_TOGGLE]: (store, payload) => {
console.log("###doneToggle!!!", payload);
store.commit(Constant.DONE_TOGGLE, payload);
}
}
src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import mutations from './mutations';
import actions from './actions';
Vue.use(Vuex);
const store = new Vuex.Store({
state,
mutations,
actions
});
export default store;
모듈 이용하기
- 저장소와 모듈은 각각가의 상태, 변이, 게터, 액션을 가짐
- 모든 모듈 저장소에서는 액션과 변이를 공유
- 모든 모듈 저장소에서는 상태나 게터는 공유하지 않음
- 간혹 모듈에서 상위 저장소의 상태를 이용해야 하는 경우가 있는데, 이 때 액션의 첫번째 인자인 store 를 통해 루트 상태에(rootState)에 접근 가능
주의할점
- 루트 저장소의 액션에서는 모듈의 상태에 접근 불가능
- 상태 데이터가 전역수준에서 이용되는지, 특정 모듈에만 이용되는지 구분필요
const module1 = {
state : { ... },
mutations : { ... },
actions : { ... },
getters : { ... }
}
const module2 = {
state : { ... },
mutations : { ... },
actions : { ... }
}
const store = new Vuex.Store({
.....
modules : {
m1 : module1,
m2 : module2,
}
});
store 객체의 속성
속성명 | 설명 |
commit | 변이를 일으키기위한 메서드 |
dispatch | 액션을 호출하기 위한 메서드. 한 액션에서 다른 액션 호출 가능 |
getters | 모듈 자기 자신의 게터 |
rootGetters | 루트 저장소의 게터 |
state | 모듈 자기 자신의 상태 데이터 |
rootState | 루트 저장소의 상태 데이터 |
contactspp_search 프로젝트 변경하기
- 다른 모듈의 액션, 변이를 이용하도록 작성할 수 있으며, 루트 상태를 활용
- 검색 기능을 모듈로 분리
- 루트 저장소에는 사용했던 검색어 리스트를 저장하도록 변경
- 액션기능 변경
- 연락처 검색이 성공적으로 완료되면 store.commit() 메서드를 이용해 상태를 변경시김.
- 검색된 연락처가 전재하는 경우에만 검색명을 루트 저장소의 keywordlist 상태 데이터에 추가하도록 addKeyword 액션을 디스패치 함
src/Constant.js
export default {
ADD_KEYWORD: 'addKeyword',
SEARCH_CONTACT: "searchContact",
BASE_URL: "http://sample.bmaster.kro.kr/contacts_long/search/"
}
src/store/module1.js
import Constant from '.../Constant';
import axios from 'axios';
export default {
state: {
contacts: []
},
mutations: {
[Constant.SEARCH_CONTACT]: (state, payload) => {
state.contacts = payload.contacts;
}
},
actions: {
[Constant.SEARCH_CONTACT]: (store, payload) => {
axios.get(Constant.BASE_URL + payload.name).then((response) => {
store.commit(Constant.SEARCH_CONTACT, { contacts: response.data });
if (response.data.length > 0) {
store.dispatch(Constant.ADD_KEYWORD, payload);
}
});
}
}
}
src/store/index.js
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;
연락처 애플리케이션에 Vuex 적용하기
contactapp 프로젝트
yarn add vuex
또는
npm install --save vuex
상수 정의
src/Constant.js
export default {
// 변이와 액션 모두 사용
ADD_CONTACT_FORM: 'addContactForm',
CANCEL_FORM: 'cancelForm',
EDIT_CONTACT_FORM: 'editContactForm',
EDIT_PHOTO_FORM: 'editPhotoForm',
FETCH_CONTACTS: 'fetchContacts',
// 액션에서만 사용
ADD_CONTACT: 'addContact', // 연락처 추가
UPDATE_CONTACT: 'updateContact', // 연락처 수정
UPDATE_PHOTO: 'updatePhoto', // 사진 수정
DELETE_CONTACT: 'deleteContact' // 연락처 삭제
}
Vue 저장소 객체 작성
src/store/status.js
import CONF from '../Config';
export default {
currentView: null,
mode: 'add',
contact: { no: 0, name: '', tel: '', address: '', photo: '' },
contactlist: { pageno: 1, pagesize: CONF.PAGESIZE, totalcount: 0, contacts: [] }
}
src/store/mutations.js
import Constant from '../Constant';
// 상태를 변경하는 기능만 구현
export default {
[Constant.ADD_CONTACT_FORM]: (state) => {
state.contact = { no: '', name: '', tel: '', address: '', photo: '' }
state.mode = 'add';
state.currentView = 'contactForm';
},
[Constant.CANCEL_FORM]: (state) => {
state.currentView = null;
},
[Constant.EDIT_CONTACT_FORM]: (state, payload) => {
state.contact = payload.contact;
state.mode = 'update';
state.currentView = 'contactForm';
},
[Constant.EDIT_PHOTO_FORM]: (state, payload) => {
state.contact = payload.contact;
state.currentView = 'updatePhoto';
},
[Constant.FETCH_CONTACTS]: (state, payload) => {
state.contactlist = payload.contactlist;
}
}
src/store/actions.js
import Constant from '../Constant';
import axios from 'axios';
import CONF from '../Config';
export default {
[Constant.ADD_CONTACT_FORM]: (store) => {
store.commit(Constant.ADD_CONTACT_FORM);
},
[Constant.ADD_CONTACT]: (store) => {
axios.post(CONF.ADD, store.state.contact).then((response) => {
if (response.data.status === 'success') {
store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: 1 });
} else {
console.log("연락처 추가 실패!! : " + response.data);
}
});
},
[Constant.EDIT_CONTACT_FORM]: (store, payload) => {
axios.get(CONF.FETCH_ONE.replace('${no}', payload.no)).then((response) => {
store.commit(Constant.EDIT_CONTACT_FORM, { contact: response.data });
});
},
[Constant.UPDATE_CONTACT]: (store) => {
var currentPageNo = store.state.contactlist.pageno;
var contact = store.state.contact;
axios.put(CONF.UPDATE.replace('${no}', contact.no), contact).then((response) => {
if (response.data.status == 'success') {
store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
} else {
console.log("연락처 변경 실패 : " + response.data);
}
});
},
[Constant.EDIT_PHOTO_FORM]: (store, payload) => {
axios.get(CONF.FETCH_ONE.replace('${no}', payload.no)).then((response) => {
store.commit(Constant.EDIT_PHOTO_FORM, { contact: response.data });
});
},
[Constant.UPDATE_PHOTO]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
var data = new FormData();
data.append('photo', payload.file);
axios.post(CONF.UPDATE_PHOTO.replace("${no}", payload.no), data).then(() => {
store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
},
[Constant.FETCH_CONTACTS]: (store, payload) => {
var pageno;
if (typeof payload === "undefined" || typeof payload.pageno === "undefined") {
pageno = 1;
} else {
pageno = payload.pageno;
}
var pagesize = store.state.contactlist.pagesize;
axios.get(CONF.FETCH, {
params: { pageno: pageno, pagesize: pagesize }
}).then((response) => {
store.commit(Constant.FETCH_CONTACTS, { contactlist: response.data });
});
},
[Constant.CANCEL_FORM]: (store) => {
store.commit(Constant.CANCEL_FORM);
},
[Constant.DELETE_CONTACT]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
axios.delete(CONF.DELETE.replace("${no}", payload.no)).then(() => {
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
}
}
src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state.js';
import mutations from './mutations.js';
import actions from './actions.js';
Vue.use(Vuex);
const store = new Vuex.Store({
state,
mutations,
actions
});
export default store;
App.js 변경
<template>
<div id="container">
<div class="page-header">
<h1 class="text-center">연락처 관리 애플리케이션</h1>
<p>(Dynamic Component + EventBus + Axios)</p>
</div>
<!--
<component :is="currentView" :contact="contact"></component>
<contact-list :contactlist="contactlist"></contact-list>
-->
<component :is="currentView"></component>
<contact-list></contact-list>
</div>
</template>
<script>
import ContactList from './components/ContactList';
import ContactForm from './components/ContactForm';
import UpdatePhoto from './components/UpdatePhoto';
import {mapState} from 'vuex';
export default {
name: 'App',
components: {
ContactList, ContactForm, UpdatePhoto
},
computed : mapState(['currentView'])
}
</script>
<style scoped>
#container {
font-family: 'Avenir', Helvetica, sans-serif;
-webkit-font-smoothing : antialised;
-webkit-osx-font-smoothing : grayscale;
text-align : center;
color : #2c3e50;
margin-top : 60px;
}
</style>
ContactList.vue 변경
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
import Paginate from 'vuejs-paginate';
export default {
name : 'contactList',
components : { Paginate },
computed : {
totalpage : function() {
return Math.floor((this.contactlist.totalcount-1) / this.contactlist.pagesize) + 1;
},
...mapState(['contactlist'])
},
watch : {
['contactlist.pageno'] : function() {
this.$refs.pagebuttons.selected = this.contactlist.pageno;
}
},
mounted : function(){
this.$store.dispatch(Constant.FETCH_CONTACTS,{pageno:1});
},
methods : {
pageChanged : function(page) {
this.$store.dispatch(Constant.FETCH_CONTACTS,{pageno:page});
},
addContact : function() {
this.$store.dispatch(Constant.ADD_CONTACT_FORM);
},
editContact : function(no) {
this.$store.dispatch(Constant.EDIT_CONTACT_FORM,{no:no});
},
deleteContact : function(no) {
if(confirm("정말로 삭제하시겠습니까?")){
this.$store.dispatch(Constant.DELETE_CONTACT,{no:no});
}
},
editPhoto : function(no) {
this.$store.dispatch(Constant.EDIT_PHOTO_FORM,{no:no});
}
}
}
</script>
ContactForm.vue 변경
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
name : 'contactForm',
computed : {
btnText : function() {
if(this.mode != 'update') return '추 가';
else return '수 정';
},
headingText : function() {
if(this.mode != 'update') return '새로운 연락처 추가';
else return '연락처 변경';
},
...mapState(['mode','contact'])
},
mounted : function() {
this.$refs.name.focus();
},
methods : {
submitEvent : function() {
if(this.mode == 'update'){
this.$store.dispatch(Constant.UPDATE_CONTACT);
}else {
this.$store.dispatch(Constant.ADD_CONTACT);
}
},
cancelEvent : function() {
this.$store.dispatch(Constant.CANCEL_FORM);
}
}
}
</script>
UpdatePhoto.vue 변경
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
name : 'updatePhoto',
//props: ['contact'],
computed : mapState(['contact']),
methods: {
cancelEvent : function() {
this.$store.dispatch(Constant.CANCEL_FORM);
},
photoSubmit : function() {
var file = this.$refs.photofile.files[0];
this.$store.dispatch(Constant.UPDATE_PHOTO,{no:this.contact.no, file:file});
this.$store.dispatch(Constant.CANCEL_FORM);
}
}
}
</script>
main.js 변경
import Vue from 'vue'
//import App from './AppAxiosTest.vue'
import App from './App.vue'
//import axios from 'axios';
import store from './store';
import 'bootstrap/dist/css/bootstrap.css';
import ES6Promise from 'es6-promise';
ES6Promise.polyfill();
//Vue.prototype.$axios = axios;
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
정리
- 단방향 데이터 흐름과정(컴포넌트 - 액션 - 변이 - 상태 - 컴포넌트)을 염두해 두고 개발
- 항상 중심이 되는 것은 상태.
- 애플리케이션 전체와 컴포넌트의 상태 데이터를 도출하고 상태를 중심으로 변이(상태를 병경시키는 상황)를 정의
- 그후 변이, 액션 순으로 설계
- 컴포넌트 UI에서 액션을 디스패치 하도록 작성
'책 > Vue.js 퀵스타트' 카테고리의 다른 글
트랜지션 효과 (0) | 2020.11.28 |
---|---|
vue-router를 이용한 라우팅 (0) | 2020.11.28 |
axios를 이용한 서버통신 (0) | 2020.11.25 |
컴포넌트 심화 (0) | 2020.11.23 |
Vue-CLI 도구 (0) | 2020.11.22 |