Vuex 코어 개념 : Mutations, Actions

🍊 Mutations

vuex에서 state를 변화시키는 유일한 방법은 mutation을 통해서다.
vuex mutation은 이벤트와 비슷한데, Mutation은 type, handler를 갖는다.

handler 함수가 state에 변화를 일으키고, 이 핸들러 함수를 호출시키는 것 store.commit(type) 이다.

핸들러 함수는 첫 번째 인자로 state를 받는다.

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})

그리고 이 핸들러 함수를 호출하는 건

store.commit('increment')
  • commit에 payload를 같이 넘기기

    store.commit을 할 때, payload라고 불리는 인자를 전달할 수 있다.
    주로 payload는 객체로 전달하여 그 의미를 잘 알 수 있도록 한다.

    ​​​​// ...
    ​​​​mutations: {
    ​​​​  increment (state, payload) {
    ​​​​    state.count += payload.amount
    ​​​​  }
    ​​​​}
    
    
    ​​​​store.commit('increment', {
    ​​​​  amount: 10
    ​​​​})
    
  • 객체 스타일의 commit

    type 프로퍼티를 추가하여 객체를 인자로 넣어 handler를 호출하는 방식도 있다. 이 때, handler 쪽은 변경하지 않아도 된다.

    ​​​​store.commit({
    ​​​​  type: 'increment',
    ​​​​  amount: 10
    ​​​​})
    
    
    ​​​​mutations: {
    ​​​​  increment (state, payload) {
    ​​​​    state.count += payload.amount
    ​​​​  }
    ​​​​}
    
    
  • 뷰의 반응형 규칙을 따르는 Mutation

    vuex store의 state는 반응적이도록 설계되었다. 따라서, state가 변경이 되면 해당 state를 Observing하는 컴포넌트는 자동으로 업데이트된다.

    따라서, mutaion을 쓸 때는 다음을 지켜줘야 한다.

    1. store의 초기 상태는 앞으로 쓰일 모든 상태를 써주는 것이 좋다.
    2. 만약 객체에 새로운 프로퍼티를 추가하고자 한다면,
      • Vue.set(obj, 'newProp', 123) 처럼 하는 방법과
      • 새로운 객체로 대체시키는 방법이 있다. 이는 spread를 사용하면 간단하게 된다.
    ​​​​state.obj = { ...state.obj, newProp: 123 }
    
  • mutation type에 상수를 사용하기

    다른 파일에 mutation의 type을 상수로 적어 export하고, store에서 import하는 방식으로 하기도 한다. 이는 linter 기능 사용, 자동완성 등 여러 이점을 준다.

    ​​​​// 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 핸들러 함수는 반드시 동기적이어야 한다는 것이다.

    ​​​​mutations: {
    ​​​​  someMutation (state) {
    ​​​​    api.callAsyncMethod(() => {
    ​​​​      state.count++
    ​​​​    })
    ​​​​  }
    ​​​​}
    

    위 예제의 문제점은 devtool이 commit이 된 시점에 callback이 실행 되었는지 아닌지를 알 방법이 없다는 것이다. 이렇게 되면, callback에서 state를 변화시켜도 추적할 수 없다는 문제가 있다.

  • mapMutations 컴포넌트에서 mutation 시키기

    mapMutations store의 Mutation을 method로 자동으로 변화시켜준다. type을 그대로 사용하려면 mapMutations에 스트링 배열을 넘기고, 이름을 바꿔 사용하려면 객체를 넘기면 된다.

    ​​​​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')`
    ​​​​    })
    ​​​​  }
    ​​​​}
    
    
  • 나의 예제

/* 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;

<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을 반복해서 쓰지 않아도 된다.

    ​​​​actions: {
    ​​​​  increment ({ commit }) {
    ​​​​    commit('increment')
    ​​​​  }
    ​​​​}
    
  • Dispatching Actions

    액션은 store.dispatch 메소드로 호출된다.

    ​​​​store.dispatch('increment');
    

    Payload를 같이 넘길 수 있는데, 이 때 payload는 객체이다.

    ​​​​// dispatch with a payload
    ​​​​store.dispatch('incrementAsync', {
    ​​​​  amount: 10
    ​​​​})
    
    ​​​​// dispatch with an object
    ​​​​store.dispatch({
    ​​​​  type: 'incrementAsync',
    ​​​​  amount: 10
    ​​​​})
    

    비동기API를 사용하고, 여러 mutation을 커밋하는 예제를 살펴보면,

    ​​​​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하기 위해서는 다음과 같이 호출해야 한다.

    ​​​​this.$store.dispatch('xxx')
    

    근데 또 이게 귀찮으니까, mapActions라는 헬퍼를 사용한다. 이 헬퍼는 컴포넌트 메소드와 store.dispatch 를 매핑시켜준다.

    아래 예제에서 this.increment()를 사용하면, increment 액션을 호출해주게 된다. 마찬가지로 payload를 넣어서 호출하게 할 수도 있다. 또한, 배열이 아닌 객체를 넣으면, 액션의 이름을 바꿀 수 있다.

    ​​​​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를 핸들링할 수 있다.

    ​​​​actions: {
    ​​​​  actionA ({ commit }) {
    ​​​​    return new Promise((resolve, reject) => {
    ​​​​      setTimeout(() => {
    ​​​​        commit('someMutation')
    ​​​​        resolve()
    ​​​​      }, 1000)
    ​​​​    })
    ​​​​  }
    ​​​​}
    
    ​​​​store.dispatch('actionA').then(() => {
    ​​​​  // ...
    ​​​​})
    

    async/await을 사용할 수도 있다.

    ​​​​// 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

/* 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로 만든 부분을 확인하기!

<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>
Select a repo