13 Vuex - 헬퍼 함수
source: categories/study/vue-beginner-lv2/vue-beginner-lv2_9-04.md
13.1 헬퍼 함수 및 ES6 Spread 연산자 소개
Vuex 기술 요소
- state: 여러 컴포넌트에 공유되는 데이터
data
- getters: 연산된 state 값을 접근하는 속성
computed
- mutations: state 값을 변경하는 이벤트 로직, 메서드
methods
- actions: 비동기 처리 로직을 선언하는 메서드
async methods
앞시간까지 Vuex 기술요소에 대해 알아봤습니다.
이제 남은건 2가지 정도입니다.
4가지 기술 요소를 좀 더 편하게 쓰게해주는 Helper 함수에 대해 알아보는 것과 마지막으로는 앞에서 배웠던 기술요소 4가지와 지금 이 시간에 배우는 Helper를 이용해서 애플리케이션이 커질 때 모듈화를 어떻게할 수 있는지,
그리고 지금은 한 store에 모든 state와 getters, mutations, actions를 몰아넣은 형태로 구현을 해놨는데,
그런 것들을 어떻게 모듈화를해서..
TodoItem이라면 item에 관련된 성격, 그리고 price면 price에 관련된 성격, 어떻게해야 이렇게 찢어가지고 관리할 수 있는지 그런 것들에 대해서 알아보도록 하겠습니다.
각 속성들을 더 쉽게 사용하는 방법 - Helper
Store에 있는 아래 4가지 속성들을 간편하게 코딩하는 방법
-
state -> mapState
this.$store.state.~~
이런식으로 접근하는 방식은 컴포넌트가 많아지면 많아질수록 불편해짐. - getters -> mapGetters
- mutations -> mapMutations
- actions -> mapActions
state, getters, mutations, actions 모두 컴포넌트가 많아지면 많아질 수록 복잡해지고 불편해진다.
이런 것들을 Vuex에 내장되어있는 mapState, mapGetters, mapMutations, mapActions를 이용해서 훨씬 더 편하게 코딩할 수 있다.
헬퍼의 사용법
- 헬퍼를 사용하고자 하는 vue 파일에서 아래와 같이 해당 헬퍼를 로딩
<!-- App.vue -->
<script>
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex';
export default {
// state, getters가 연관이 있는 부분이 computed
// state, getters - 상태로 접근해서 템플릿에 표현해주는 속성들. 그래서 computed에 들어간다.
computed: {
// num은 store의 state에 저장되어있는 num을 뜻합니다. 그거를 들고와서 computed에 맵핑한거라고 보시면됩니다.
// 이 컴포넌트에서 this.num을 하면 store에 있는 state의 num을 가리키는 것과 동일합니다.
...mapState(['num']),
// countedNum도 마찬가지. 이 컴포넌트에서 this.countedNum으로 접근 가능합니다.
// mapState, mapGetters 등으로 이러한 것들을 편하게 구현할 수 있습니다.
// 만약에 mapState나 mapGetters를 안썼으면 this.$store.state.num, this.$store.getters.countedNum으로 접근했어야됩니다.
// 그런것들을 좀 더 편하게 할 수 있도록 만들어주는겁니다.
...mapGetters(['countedNum'])
},
methods: {
// 이 컴포넌트에서 v-on:click="clickBtn"라고 적어도 store에 정의된 mutations의 clickBtn 메소드가 실행됩니다.
...mapMutations(['clickBtn']),
// actions도 마찬가지
...mapActions(['asyncClickBtn'])
}
}
</script>
Q. ...
는 오타인가요? ES6의 Object Spread Operator입니다.
let josh = {
field: 'web',
language: 'js'
}
let developer = {
nation: 'korea',
...josh
}
console.log(developer); // {nation: 'korea', field: 'web', language: 'js'}
13.2 mapState, mapGetters 소개 및 ES6 Spread 연산자 쓰는 이유
mapState
- Vuex에 선언한 state 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼
<!-- App.vue -->
<template>
<!-- <p>{{ this.$store.state.num }}</p> -->
<p>{{ this.num }}</p>
</template>
<script>
import {mapState} from 'vuex';
export default {
computed: {
...mapState(['num'])
// num() {return this.$store.state.num} // 이런식으로 코드가 들어가고 state의 num에 접근하는 것.
}
}
</script>
// store.js
state: {
num: 10
}
mapGetters
- Vuex에 선언한 getters 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼
<!-- App.vue -->
<template>
<!-- <p>{{ this.$store.getters.reverseMessage }}</p> -->
<p>{{ this.reverseMessage }}</p>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
computed: {
...mapGetters(['reverseMessage'])
}
}
</script>
// store.js
getters: {
reverseMessage(state) {
return state.msg.split('').reverse().join('');
}
}
13.3 [리팩토링] getters와 mapGetters 적용하기
vue 권고사항
template 태그 안에선 가급적 자바스크립트 연산이라던지 전체적인 속성 접근을 줄이는 방향으로 진행한다.
template 태그를 최대한 깔끔하게 만든다.
깔끔하게 표현하기 위한 연산들은 전부 다 script(컴포넌트의 내부로직) 안에서 한다.
일단 현재 상태에서 getters 적용해보기
src/store/store.js
import Vue from "vue";
import Vuex from "vuex";
// vue 플러그인을 사용하는 코드
// use는 vue의 플러그인을 등록하는 기능을한다.
// use를 쓰는 이유는 일반적으로 vue를 사용할 때, 전역으로.. 그러니가 vue를 사용하는 모든 영역에 특정 기능을 추가하고 싶을 때,
// 글로벌 펑셔널리티를 추가하고싶을 때, 사용합니다.
Vue.use(Vuex);
const storage = {
fetch() {
const arr = [];
if (localStorage.length > 0) {
for (let i=0; i < localStorage.length; i++) {
if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
arr.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
}
return arr;
}
}
export const store = new Vuex.Store({
state: {
todoItems: storage.fetch()
},
getters: {
storedTodoItems(state) {
return state.todoItems;
}
},
mutations: {
addOneItem(state, todoItem) {
const obj = {completed: false, item: todoItem};
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
removeOneItem(state, {todoItem, index}) {
localStorage.removeItem(todoItem.item);
state.todoItems.splice(index, 1);
},
toggleOneItem(state, {todoItem, index}) {
state.todoItems[index].completed = !state.todoItems[index].completed;
// todoItem.completed = !todoItem.completed; // deep copy를 해서 값을 변형시킨게 아니라 참조값을 변형시킨것이므로 todoItems 값에 변형이 가해진다.
// 때문에 todoItems 값을 따로 업데이트 안해도된다. 그렇기 때문에 바로바로 화면 렌더링이 일어난다.
localStorage.removeItem(todoItem.item); // 해당 키로 삭제
localStorage.setItem(todoItem.item, JSON.stringify(todoItem)); // 키와 값 다시 재등록
},
clearAllItems(state) {
localStorage.clear();
state.todoItems = [];
}
}
});
// 간단한 설치와함께 Vuex 등록까지 마친상태
src/components/TodoList.vue
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, index) in this.todoItems" v-bind:key="todoItem.item" class="shadow">
<i class="checkBtn fas fa-check"
v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleComplete(todoItem, index)"
></i>
<span v-bind:class="{textCompleted: todoItem.completed}">{{todoItem.item}}</span>
<span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
export default {
name: "TodoList",
methods: {
removeTodo(todoItem, index) {
this.$store.commit('removeOneItem', {todoItem, index});
},
toggleComplete(todoItem, index) {
this.$store.commit('toggleOneItem', {todoItem, index});
}
},
computed: {
todoItems() {
return this.$store.getters.storedTodoItems
}
}
}
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 0;
margin-top: 0;
text-align: left;
}
li {
display: flex;
min-height: 50px;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background-color: #ffffff;
border-radius: 5px;
}
.checkBtn {
line-height: 45px;
color: #62acde;
margin-right: 5px;
}
.checkBtnCompleted {
color: #b3adad;
}
.textCompleted {
text-decoration: line-through;
color: #b3adad;
}
.removeBtn {
margin-left: auto;
color: #de4343;
}
/* 리스트아이템 트랜지션 효과 */
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
mapGetters 적용해보기
src/components/TodoList.vue
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, index) in this.storedTodoItems" v-bind:key="todoItem.item" class="shadow">
<i class="checkBtn fas fa-check"
v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleComplete(todoItem, index)"
></i>
<span v-bind:class="{textCompleted: todoItem.completed}">{{todoItem.item}}</span>
<span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: "TodoList",
methods: {
removeTodo(todoItem, index) {
this.$store.commit('removeOneItem', {todoItem, index});
},
toggleComplete(todoItem, index) {
this.$store.commit('toggleOneItem', {todoItem, index});
}
},
computed: {
// todoItems() {
// return this.$store.getters.storedTodoItems
// }
...mapGetters(['storedTodoItems'])
// 만약 storedTodoItems 이름이 쓰기싫고 todoItems라는 이름을 쓰고싶다면?
// 아래와같이 객체를 활용하면된다.
// 아래와 같이하면 this.todoItems로 사용할 수 있다.
// ...mapGetters({
// todoItems: 'storedTodoItems'
// })
}
}
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 0;
margin-top: 0;
text-align: left;
}
li {
display: flex;
min-height: 50px;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background-color: #ffffff;
border-radius: 5px;
}
.checkBtn {
line-height: 45px;
color: #62acde;
margin-right: 5px;
}
.checkBtnCompleted {
color: #b3adad;
}
.textCompleted {
text-decoration: line-through;
color: #b3adad;
}
.removeBtn {
margin-left: auto;
color: #de4343;
}
/* 리스트아이템 트랜지션 효과 */
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
13.4 mapMutations, mapActions 소개 및 헬퍼의 유연한 문법
mapMutations
- Vuex에 선언한 mutations 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼
<!-- App.vue -->
<template>
<button @click="clickBtn">popup message</button>
</template>
<script>
import {mapMutations} from 'vuex';
export default {
methods: {
// 위와 같이 @click="clickBtn"으로 clickBtn 메소드가 실행되지만
// 실제 로직은 store에 있는걸 가져다쓴다.
...mapMutations(['clickBtn']),
authLogin() {},
displayTable() {}
}
}
</script>
// store.js
mutations: {
clickBtn(state) {
alert(state.msg);
}
}
mapActions
- Vuex에 선언한 actions 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼
<!-- App.vue -->
<template>
<button @click="delayClickBtn">delay popup message</button>
</template>
<script >
import {mapActions} from 'vuex';
export default {
methods: {
...mapActions(['delayClickBtn']),
}
}
</script>
// store.js
actions: {
delayClickBtn(context) {
setTimeout(() => context.commit('clickBtn'), 2000);
}
}
헬퍼의 유연한 문법
-
Vuex에 선언한 속성을 그대로 컴포넌트에 연결하는 문법
// 배열 리터럴 ...mapMutations([ 'clickBtn', // clickBtn: clickBtn 'addNumber' // addNumber(인자) - 이렇게 addNumber에 인자가 들어가면, 인자까지 선언해줘야하는데, 그런걸 선언 안해줘도 자연스럽게 인자를 넘겨줍니다. // store에 addNumber(state, index) 이렇게 정의되어있다면 여기서 state, index가 자동으로 넘어간다는 말인가? 그런 말인거 같긴한데 흠자 // 아.. 이 말이 이렇게 선언하고 template 부분에 clickBtn(인자1, 인자2..) 이렇게 넘겨도 된다는 말이었네.. 근데 그건 당연한거 아닌가..? 당연한걸 좋다고 표현한 느낌..? // 여튼 이런 부분들이 굉장히 편합니다. ])
-
Vuex에 선언한 속성을 컴포넌트의 특정 메서드에다가 연결하는 문법
// 객체 리터럴 ...mapMutations({ popupMsg: 'clickBtn' // 컴포넌트 메서드명 : Store의 뮤테이션명 })
13.5 [리팩토링&퀴즈] mapMutations 적용 및 퀴즈
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, index) in this.storedTodoItems" v-bind:key="todoItem.item" class="shadow">
<i class="checkBtn fas fa-check"
v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleOneItem({todoItem, index})"
></i>
<span v-bind:class="{textCompleted: todoItem.completed}">{{todoItem.item}}</span>
<span class="removeBtn" v-on:click="removeOneItem({todoItem, index})">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
import {mapGetters, mapMutations} from 'vuex';
export default {
name: "TodoList",
methods: {
...mapMutations(['removeOneItem', 'toggleOneItem']),
// removeTodo(todoItem, index) {
// this.$store.commit('removeOneItem', {todoItem, index});
// },
// toggleComplete(todoItem, index) {
// this.$store.commit('toggleOneItem', {todoItem, index});
// }
},
computed: {
// todoItems() {
// return this.$store.getters.storedTodoItems
// }
...mapGetters(['storedTodoItems'])
// 만약 storedTodoItems 이름이 쓰기싫고 todoItems라는 이름을 쓰고싶다면?
// 아래와같이 객체를 활용하면된다.
// 아래와 같이하면 this.todoItems로 사용할 수 있다.
// ...mapGetters({
// todoItems: 'storedTodoItems'
// })
}
}
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 0;
margin-top: 0;
text-align: left;
}
li {
display: flex;
min-height: 50px;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background-color: #ffffff;
border-radius: 5px;
}
.checkBtn {
line-height: 45px;
color: #62acde;
margin-right: 5px;
}
.checkBtnCompleted {
color: #b3adad;
}
.textCompleted {
text-decoration: line-through;
color: #b3adad;
}
.removeBtn {
margin-left: auto;
color: #de4343;
}
/* 리스트아이템 트랜지션 효과 */
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
13.6 [리팩토링&퀴즈] mapMutations 퀴즈 풀이
<template>
<div class="clearAllContainer">
<span class="clearAllBtn" v-on:click="clearAllItems">
Clear All
</span>
</div>
</template>
<script>
import {mapMutations} from "vuex";
export default {
name: "TodoFooter",
methods: {
...mapMutations(['clearAllItems']),
// clearTodo() {
// this.$store.commit('clearAllItems');
// }
}
}
</script>
<style scoped>
.clearAllContainer {
width: 8.5rem;
height: 50px;
line-height: 50px;
background-color: #ffffff;
border-radius: 5px;
margin: 0 auto;
}
.clearAllBtn {
color: #e20303;
display: block;
}
</style>
13.7 헬퍼 함수가 주는 간편함
여기선 getters에 대해 살펴보겠습니다.
mapGetters를 쓰면 어느 부분에서 코딩을 더 편하게할 수 있고 타이핑을 덜할수 있는지 한번 살펴보도록 하겠습니다.
src/store
폴더 안에 demoStore.js
파일을 생성합니다.
예시 파일입니다.
아래와 같이 코드를 간결하고 이쁘게 정리할 수 있다 정도로 보시면될 거 같습니다.
src/store/demoStore.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
price: 100,
},
getters: {
originalPrice(state) {
return state.price;
},
doublePrice(state) {
return state.price * 2;
},
triplePrice(state) {
return state.price * 3;
}
}
})
src/components/Demo.vue
<template>
<div id="root">
<!-- vue.js 권고사항: 탬플릿에 연결하는 함수, 속성들 최대한 심플하게 연결해라 -->
<p>{{originalPrice}}</p>
<p>{{doublePrice}}</p>
<p>{{triplePrice}}</p>
</div>
</template>
<script>
import {mapGetters} from "vuex";
export default {
name: "Demo",
computed: {
...mapGetters(['originalPrice', 'doublePrice', 'triplePrice']),
// originalPrice() {
// return this.$store.getters.originalPrice;
// },
// doublePrice() {
// return this.$store.getters.doublePrice
// },
// triplePrice() {
// return this.$store.getters.triplePrice
// }
}
}
</script>
<style scoped>
</style>
13.8 Vuex로 리팩토링한 애플리케이션 구조 분석 및 정리
원래는 App.vue
에 데이터를 처리하는 모든 로직이 들어가있었다.
이를 컨테이너 컴포넌트라고 표현했었다.
컨테이너 컴포넌트란 비즈니스 로직을 처리하는 컴포넌트를 뜻했다.
그래서 실질적으로 컨테이너 컴포넌트엔 마크업이 존재하지 않는다.
보통 컨테이너 컴포넌트엔 프레젠테이셔널 컴포넌트를 등록하기만한다.
프레젠테이셔널 컴포넌트라고하면 App.vue
의 하위 컴포넌트들이 컨테이너 컴포넌트인 App.vue
로부터 데이터를 props
로 다 받아서 그 데이터를 표현해주기만 한다.
그리고 하위 컴포넌트(프레젠테이셔널 컴포넌트)에서 데이터를 조작할 일이 생긴다면,
그 조작 요청을 컨테이너 컴포넌트에 event emit
을 통해 보내는 것이 보통 프레젠테이셔널 컴포넌트의 역할이다.
이것이 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트의 차이점이다.
App.vue
에서 그런식으로 로직을 처리했었고, 하위 컴포넌트에서 event emit
을 통해 App.vue
에서 받아서 데이터 조작을 했었다.
Vuex 적용 후
App.vue
코드 내용이 아주 깔끔해졌다.
원래 Vuex를 적용 전에는 App.vue
가 컨테이너 컴포넌트 역할을 했었는데, Vuex를 적용하고나서 App.vue
는 루트 컴포넌트로써 하위 컴포넌트를 등록하는 것 말고는 아무것도 없습니다.
App.vue
에 TodoHeader
, TodoFooter
, TodoList
, TodoInput
컴포넌트를 등록한 것 밖에는 없는겁니다.
그리고 기존에 App.vue
에 있던 로직들이 src/store/store.js
파일로 모두 옮겨갔습니다.
state
, getters
, mutations
, actions
를 이용해서 코드를 수정했습니다.
이렇게 store
로 로직을 옮기면서 원래는 App.vue
에서 하위 컴포넌트로 데이터를 내려주던 것들(props
), 이벤트를 캐칭하던 속성들 전부 다 사라졌습니다.
TodoList.vue
를 봐도 기존에 this.$emit()
으로 이벤트 발생시키던 것도 다 없어지고 mapGetters
, mapMutations
, mapStates
, mapActions
로 애플리케이션 로직 처리를 전부 다 store
로 넘겼습니다.
다른 컴포넌트들도 마찬가지입니다.
이렇게 Vuex로 코드를 깔끔하게 정리할 수 있습니다.
앞으로 저희가 볼 것은 프로젝트 구조화, 모듈화입니다.
현재는 src/store/store.js
파일 하나에 모든 mutations
, state
, getters
, actions
가 다 들어가있잖아요?
그럼 코드가 막 몇백줄, 몇천줄 된다고하면…
그 코드들 보고 싶을까요?
한 파일 안에 메소드들이 왕창 들어가면 되게 보기싫어집니다.
이런 것들을 ES6의 모듈기능(import
, export
)을 활용해서 파일을 다 찢을 수 있습니다.
제가 이런 말씀을 드리는 이유는 다음 시간부터 볼 부분이, store
에 메소드들이 많아진다면 그 메서드들이 항상 동일한 속성들과 연관이있진 않을거란말이죠?
state
가 많아지고 그러면서 각 state
에 관련된 메소드들도 천차만별이될테고, 그렇게되면 관리하기 너무 복잡해질 것입니다.
이런 부분들을 ES6의 모듈기법을 통해서 구조화해보도록 하겠습니다.