Vuex 코어 개념 : state, getters

vuex는 vue.js의 상태관리 라이브러리이다.

여러 컴포넌트에서 공통된 state를 공유하고자할 때, 발생할 수 있는 문제점들이 있다. 첫 번째는 props를 깊은 컴포넌트에게로 전달해야 하는 문제이다. 두 번째는 직접 부모/자식 인스턴스를 참조하거나, 이벤트를 통해 상태의 여러 복사본을 바꾸거나, 동기화시켜야 하는 등의 해결 방법을 써야 한다.

상태 관리와 뷰를 독립적으로 관리하여, 코드르 좀 더 유지하기 쉽고, 구조적으로 만들 수 있다.

Vuex의 기본 아이디어는 Flux, Redux, The Elm Architecture에 있다.

작은 SPA를 만든다면, vuex의 store pattern을 사용해도 좋다. SPA의 사이즈가 커진다면, vuex를 사용하여 상태관리를 하는 것이 더 낫다.

vuex store를 Root Component에 등록하기

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

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

위에 있는 가장 간단한 스토어를 만들었으면, store.state로 이 스토어의 state에 접근할 수 있다. vue component에서 this.$store로 스토어에 접근하게 하려면, Vue Instance에 생성된 스토어를 추가해야 한다.
Vuex에는 스토어를 Root Component의 store 옵션에 둠으로써, 스토어를 모든 child component에 inject할 수 있다.

new Vue로 생성한 root component의 store 프로퍼티에 스토어를 추가하자.

new Vue({
  el: '#app',
  store
})

Core Concepts

State

vuex는 single state tree를 사용한다. 이는 "single source of truth"에 따라 하나의 오브젝트에 모든 상태를 저장하고, 제공해주는 것을 의미한다.
single state tree도 modularity를 제공한다. state를 나눌 수도 있고, mutation을 하위 모듈로 나눌 수 있다.

  • vuex의 data는 plain object여야 한다.

  • inject the store into all child components

    vuex는 스토어를 루트 컴포넌트에 store 옵션으로 넣을 수 있게 하였다. 루트에 스토어를 주입함으로써 하위 컴포넌트에도 스토어가 주입되어 this.$store로 스토어에 접근할 수 있게 된다.

  • inject하지 않고 모듈 시스템을 사용하여 import해오는 것의 문제는?

    1. 해당 스토어의 상태를 쓰는 모든 컴포넌트에서 스토어를 import해와야 한다.
    2. 컴포넌트 테스트할 때, mocking해야 한다.

    mobx에서도 스토어를 사용하려면 컴포넌트에서 스토어를 import하거나 inject하거나 둘 중 하나였다. 사실 inject하려면 결국 또 import해오는 것이어서, 둘의 차이를 알 수 없었다.

    vuex에서는 root에서만 등록하고, subcomponent에서는 아예 import해오는 일이 없으면 inject가 더 편할 것 같긴하다.

    컴포넌트 테스트는 개인 플젝할 때는 전혀 고려하지 않은 사항인데, 현업에서는 컴포넌트 또한 테스트를 하는 것 같아서 고려할 대상이 되는 것 같다.

  • mapState helper

    컴포넌트 내에서 프로퍼티를 사용하려면, this.$store.state.count로 접근해야 하는데, 이게 귀찮으니 computed를 만들어서 state를 가져오게 된다.

    ​​​​// let's create a Counter component
    ​​​​const Counter = {
    ​​​​  template: `<div>{{ count }}</div>`,
    ​​​​  computed: {
    ​​​​    count () {
    ​​​​      return store.state.count
    ​​​​    }
    ​​​​  }
    ​​​​}
    

    그러나, 이 computed를 만드는 작업도 귀찮다.
    mapState helper가 있으면, 자동으로 computed getter 함수를 만들 수 있다.

    ​​​​// in full builds helpers are exposed as Vuex.mapState
    ​​​​import { mapState } from 'vuex'
    
    ​​​​export default {
    ​​​​  // ...
    ​​​​  computed: mapState({
    ​​​​    // arrow functions can make the code very succinct!
    ​​​​    count: state => state.count,
    
    ​​​​    // passing the string value 'count' is same as `state => state.count`
    ​​​​    countAlias: 'count',
    
    ​​​​    // to access local state with `this`, a normal function must be used
    ​​​​    countPlusLocalState (state) {
    ​​​​      return state.count + this.localCount
    ​​​​    }
    ​​​​  })
    ​​​​}
    

    mapped computed property의 이름이 state sub tree name과 같을 떄, 스트링 배열을 mapState에 넘길 수 있다. count: state => state.count,이걸 한 번에 해주는 것이다.

    ​​​​computed: mapState([
    ​​​​  // map this.count to store.state.count
    ​​​​  'count'
    ​​​​])
    
  • mapState와 기존 computed를 합치려면 ? object spread operator

    ​​​​computed: {
    ​​​​  localComputed () { /* ... */ },
    ​​​​  // mix this into the outer object with the object spread operator
    ​​​​  ...mapState({
    ​​​​    // ...
    ​​​​  })
    ​​​​}
    

Getters

store에서의 getter는 store의 computed라고 생각하면 된다.
같은 computed를 중복해서 여러 컴포넌트에서 정의할거라면,
store에 getter를 정의하는 것으로 하면 된다.

component의 computed와 마찬가지로 store의 getter 또한 getter가 의존하고 있는 것에 따라 캐싱된다.
즉, 의존하고 있는 상태값이 변경될 때에만 다시 계산된다.

getter의 첫 번째 인자는 state이다.

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  • 프로퍼티처럼 getter에 접근하기
    getters는 store.getters 객체로 보여진다.
    만약 어떠한 getter에서 다른 getter에 접근하고자 하면, 두 번째 인자로 getters를 넘겨서 접근하면 된다.

    ​​​​getters: {
    ​​​​  // ...
    ​​​​  doneTodosCount: (state, getters) => {
    ​​​​    return getters.doneTodos.length
    ​​​​  }
    ​​​​}
    

    컴포넌트에서는 이 getter를 바로 가져다가 사용하면 된다.

    ​​​​computed: {
    ​​​​  doneTodosCount () {
    ​​​​    return this.$store.getters.doneTodosCount
    ​​​​  }
    ​​​​}
    
  • 메소드처럼 getter에 접근하기

    getter가 함수를 반환하게 하면, 이 함수에 인자를 넣어서 query할 수 있다. closure를 활용한 것!

    ​​​​getters: {
    ​​​​  // ...
    ​​​​  getTodoById: (state) => (id) => {
    ​​​​    return state.todos.find(todo => todo.id === id)
    ​​​​  }
    ​​​​}
    
    ​​​​store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
    
  • mapGetters Helper

    Store에 있는 getter를 사용하기 위해 컴포넌트에서는 매 번 computed로 바꿔주었다.

    ​​​​computed: {
    ​​​​  doneTodosCount () {
    ​​​​    return this.$store.getters.doneTodosCount
    ​​​​  }
    ​​​​}
    

    이 작업을 자동으로 해주는게 mapGetters이다. 이것은 store의 getter를 가져와서 local computed 프로퍼티로 만들어준다.

    ​​​​import { mapGetters } from 'vuex'
    
    ​​​​export default {
    ​​​​  // ...
    ​​​​  computed: {
    ​​​​        // mix the getters into computed with object spread operator
    ​​​​        ...mapGetters([
    ​​​​          'doneTodosCount',
    ​​​​          'anotherGetter',
    ​​​​          // ...
    ​​​​        ])
    ​​​​      }
    ​​​​    }
    
    

    만약 getter의 이름을 바꾸고 싶다면, 객체를 사용하면 된다.

    ​​​​...mapGetters({
    ​​​​  // map `this.doneCount` to `this.$store.getters.doneTodosCount`
    ​​​​  doneCount: 'doneTodosCount'
    ​​​​})
    
  • 직접 해본 count 예제

    스토어!

    ​​​​import Vue from 'vue';
    ​​​​import Vuex from 'vuex';
    
    ​​​​Vue.use(Vuex);
    
    ​​​​const store = new Vuex.Store({
    ​​​​  state: {
    ​​​​    count: 100
    ​​​​  },
    ​​​​  mutations: {
    ​​​​    increment(state) {
    ​​​​      state.count += 1;
    ​​​​    }
    ​​​​  },
    ​​​​  getters: {
    ​​​​    minusCount(state) {
    ​​​​      return state.count * -1;
    ​​​​    },
    ​​​​    minusDoubleCount(state, getters) {
    ​​​​      return getters.minusCount * 2;
    ​​​​    },
    ​​​​    plusCount: state => num => state.count + num
    ​​​​  }
    ​​​​});
    
    ​​​​export default store;
    
    

    컴포넌트!

    ​​​​import { mapState, mapGetters } 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와 이름이 겹쳐서 안됨.
    ​​​​      countAlias: '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'
    ​​​​    })
    ​​​​  }
    ​​​​};
    
Select a repo