owned this note
owned this note
Published
Linked with GitHub
---
tags: vuex
---
# Vuex 코어 개념 : Mutations, Actions
## 🍊 Mutations
**vuex에서 state를 변화시키는 유일한 방법은 mutation을 통해서다.**
vuex mutation은 이벤트와 비슷한데, Mutation은 `type`, `handler`를 갖는다.
**handler 함수**가 state에 변화를 일으키고, 이 핸들러 함수를 호출시키는 것 **store.commit(type)** 이다.
핸들러 함수는 첫 번째 인자로 state를 받는다.
```javascript
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})
```
그리고 이 핸들러 함수를 호출하는 건
```javascript
store.commit('increment')
```
- commit에 payload를 같이 넘기기
store.commit을 할 때, **payload**라고 불리는 인자를 전달할 수 있다.
<u>주로 payload는 **객체**로 전달하여 그 의미를 잘 알 수 있도록 한다.</u>
```javascript
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
```
```javascript
store.commit('increment', {
amount: 10
})
```
- 객체 스타일의 commit
type 프로퍼티를 추가하여 객체를 인자로 넣어 handler를 호출하는 방식도 있다. 이 때, handler 쪽은 변경하지 않아도 된다.
```javascript
store.commit({
type: 'increment',
amount: 10
})
```
```javascript
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
```
- **뷰의 반응형 규칙을 따르는 Mutation**
vuex store의 state는 반응적이도록 설계되었다. 따라서, state가 변경이 되면 해당 state를 Observing하는 컴포넌트는 자동으로 업데이트된다.
따라서, <u>mutaion을 쓸 때는 다음을 지켜줘야 한다.</u>
1. **store의 초기 상태는 앞으로 쓰일 모든 상태를 써주는 것**이 좋다.
2. 만약 객체에 새로운 프로퍼티를 추가하고자 한다면,
- `Vue.set(obj, 'newProp', 123)` 처럼 하는 방법과
- **새로운 객체로 대체시키는 방법**이 있다. 이는 spread를 사용하면 간단하게 된다.
```javascript
state.obj = { ...state.obj, newProp: 123 }
```
- mutation type에 상수를 사용하기
다른 파일에 mutation의 type을 상수로 적어 export하고, store에서 import하는 방식으로 하기도 한다. 이는 linter 기능 사용, 자동완성 등 여러 이점을 준다.
```javascript
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION] (state) {
// mutate state
}
}
})
```
- mutaion은 반드시 동기적(Synchronous)
중요한 포인트는 📌 **mutation 핸들러 함수는 반드시 동기적**이어야 한다는 것이다.
```javascript
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
```
위 예제의 문제점은 devtool이 commit이 된 시점에 callback이 실행 되었는지 아닌지를 알 방법이 없다는 것이다. 이렇게 되면, callback에서 state를 변화시켜도 추적할 수 없다는 문제가 있다.
- `mapMutations` 컴포넌트에서 mutation 시키기
`mapMutations` store의 Mutation을 method로 자동으로 변화시켜준다. type을 그대로 사용하려면 `mapMutations`에 스트링 배열을 넘기고, 이름을 바꿔 사용하려면 객체를 넘기면 된다.
```javascript
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // map `this.increment()` to `this.$store.commit('increment')`
// `mapMutations` also supports payloads:
'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
})
}
}
```
- 나의 예제
```javascript
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const ADD_COUNT = 'ADD_COUNT';
const store = new Vuex.Store({
state: {
count: 100
},
mutations: {
[ADD_COUNT](state, payload) {
state.count += payload.num;
}
}
});
export default store;
```
```htmlembedded
<template>
<header id="header">
<button @click="addToCount({ num: 10 })">click here</button>
store : {{ storeCount }}
</header>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
export default {
computed: {
...mapState({
storeCount: 'count', // string으로 넘기면, state => state.count와 같다.
}),
},
methods: {
...mapMutations({ addToCount: 'ADD_COUNT' })
}
};
</script>
```
## 🍊 Actions
mutation은 반드시 동기적인 transaction이어야 하는데, 그럼 비동기 operation은 어떻게 할까? 바로 Action!
Action은 다음과 같은 특징을 갖는다.
1. mutation을 커밋한다. (mutation은 상태를 변화시킨다.)
2. action 안에서 비동기 처리를 할 수 있다.
액션 핸들러는 `context`객체라는 것을 받는다.
이 context 객체는 **store instance에서 쓰이는 메소드와 프로퍼티의 set과 동일**한 것이다. 따라서, `store.commit`을 하여 mutation을 시켰던 것처럼, 액션 안에서 `context.commit`을 하여 mutation을 커밋할 수 있다.
**스토어에 있는 State나 getter에 접근하려면 `context.state`, `context.getters`로 접근**할 수 있다. **다른 액션을 `context.dispatch`로 호출**할 수도 있다.
정리하자면, 액션은 `context` 객체를 인자로 받으며,
액션안에서...
- state 접근 : `context.state`
- getters 접근 : `context.getters`
- mutation commit : `context.commit`
- 다른 액션 호출 : `context.dispatch`
- 비구조화로 commit을 바로 가져와서 쓰기
context.commit을 반복해서 쓰지 않아도 된다.
```javascript
actions: {
increment ({ commit }) {
commit('increment')
}
}
```
- Dispatching Actions
액션은 `store.dispatch` 메소드로 호출된다.
```javascript
store.dispatch('increment');
```
Payload를 같이 넘길 수 있는데, 이 때 payload는 객체이다.
```javascript
// dispatch with a payload
store.dispatch('incrementAsync', {
amount: 10
})
// dispatch with an object
store.dispatch({
type: 'incrementAsync',
amount: 10
})
```
비동기API를 사용하고, 여러 mutation을 커밋하는 예제를 살펴보면,
```javascript
actions: {
checkout ({ commit, state }, products) {
// save the items currently in the cart
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
commit(types.CHECKOUT_REQUEST)
// the shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => commit(types.CHECKOUT_SUCCESS),
// handle failure
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
```
- 컴포넌트에서 Action dispatch하기 : `mapActions`
컴포넌트 내에서 액션을 dispatch하기 위해서는 다음과 같이 호출해야 한다.
```javascript
this.$store.dispatch('xxx')
```
근데 또 이게 귀찮으니까, `mapActions`라는 헬퍼를 사용한다. 이 헬퍼는 컴포넌트 메소드와 `store.dispatch` 를 매핑시켜준다.
아래 예제에서 this.increment()를 사용하면, increment 액션을 호출해주게 된다. 마찬가지로 payload를 넣어서 호출하게 할 수도 있다. 또한, 배열이 아닌 객체를 넣으면, 액션의 이름을 바꿀 수 있다.
```javascript
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // map `this.increment()` to `this.$store.dispatch('increment')`
// `mapActions` also supports payloads:
'incrementBy' // map `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // map `this.add()` to `this.$store.dispatch('increment')`
})
}
}
```
- Composing Actions
액션은 주로 비동기적이다. 그럼 어떻게 액션이 끝났는지를 알 수 있을까? 또한, 어떻게 여러 액션을 같이 구성할 수 있을까?
action을 dispatch하는 `store.action`은 액션 핸들러가 반환하는 Promise를 핸들링할 수 있다.
```javascript
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
```
```javascript
store.dispatch('actionA').then(() => {
// ...
})
```
`async/await`을 사용할 수도 있다.
```javascript
// assuming `getData()` and `getOtherData()` return Promises
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for `actionA` to finish
commit('gotOtherData', await getOtherData())
}
}
```
- my action example
store
```javascript
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const ADD_COUNT = 'ADD_COUNT';
const store = new Vuex.Store({
state: {
count: 100
},
mutations: {
[ADD_COUNT](state, payload) {
state.count += payload.num;
}
},
actions: {
incrementAsync({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit(ADD_COUNT, { num: 300 });
resolve();
}, 1000);
});
}
}
});
export default store;
```
component : `mapActions`로 액션을 method로 만든 부분을 확인하기!
```htmlembedded
<template>
<header id="header">
<button @click="addToCount({ num: 10 })">click here</button>
<button @click="incrementAsync300">async added number</button>
store : {{ storeCount }}
</header>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
data() {
return {
title: "Lillie's Trello",
count: 'this is local count'
};
},
computed: {
// ...mapState(['count']), // 원래는 유효하지만, 이 예제에서는 local state의 count와 이름이 겹쳐서 안됨.
...mapState({
// count: state => state.count,
// 원래는 유효하지만, 이 예제에서는 local state의 count와 이름이 겹쳐서 안됨.
storeCount: 'count', // string으로 넘기면, state => state.count와 같다.
sayLocalCount(state) {
// hi this is local count, this is count from store : 100
return `hi ${this.count}, this is count from store : ${state.count}`;
}
}),
myCount() {
return this.$store.getters.plusCount(10);
},
...mapGetters(['minusCount']),
...mapGetters({
mdCount: 'minusDoubleCount'
})
},
methods: {
...mapMutations({ addToCount: 'ADD_COUNT' }),
...mapActions({ incrementAsync300: 'incrementAsync' })
}
};
</script>
```