티스토리 뷰

Vue.JS

[vuex] Modules

버미노트 2019. 7. 16. 01:05

하나의 state 트리를 사용하기 때문에 어플리케이션의 모든 state는 하나의 큰 객체 안에 포함됩니다. 프로젝트의 규모가 커질 수록 store의 크기도 커지게 됩니다. Vuex는 이 문제를 해결하기 위해 store를 모듈로 나눌 수 있는 기능을 제공합니다. 각 모듈은 state, mutation, action, getter, 중첩된 모듈을 포함 할 수 있습니다.

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA'의 상태
store.state.b // -> moduleB'의 상태

1. Module Local State

모듈의 mutation과 getter의 핸들러의 첫 번째 전달인자는 모듈의 로컬 state가 됩니다.

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // state는 지역 모듈 상태 입니다
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

action의 핸들러의 첫번째 인자는 context입니다. 내부 모듈에서는 context.state는 로컬 state를 나타내고, 루트 state는 context.rootState로 노출됩니다.

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

getter의 핸들러에서도 루트 state를 접근할 수 있습니다. getter의 핸들러의 세번째 인자는 루트 state가 됩니다.

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

2. Namespace

기본적으로 모듈 내의 action, mutation, getter는 전역 네임스페이스 아래에 등록됩니다. 여러 모듈의 mutation, action의 핸들러를 동일한 이름으로 만들었다면, 여러 모듈이 동일한 mutation 혹은 action에 반응하게 됩니다.
만약 모듈이 독립적으로 재사용되기 원한다면, namespaced: true를 사용하면 됩니다. 모듈이 등록 될 때, 해당 모듈의 모든 getter, action, mutation은 자동으로 모듈의 경로를 기반으로 네임스페이스가 지정됩니다.

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 모듈 자산
      state: { ... }, // 모듈 상태는 이미 중첩되어 있고, 네임스페이스 옵션의 영향을 받지 않음
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 중첩 모듈
      modules: {
        // 부모 모듈로부터 네임스페이스를 상속받음
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 네임스페이스를 더 중첩
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

네임스페이스의 getter, action은 로컬의 getters, dispatch, commit을 전달 받습니다. 즉 동일한 모듈 안에서는 접두어 없이 모듈의 자산(actions, mutations, getters, state)들을 사용할 수 있습니다.

네임스페이스 모듈에서 전역 접근

모듈에서 전역의 state나 getter를 사용해야 한다면, rootStaterootGetters가 getter의 핸들러 함수의 3번째, 4번째 인자로 전달 됩니다. 또한 action의 핸들러 함수의 context 객체의 속성으로도 전역에 접근할 수 있습니다.
전역의 action을 dispatch 하거나, mutation을 commit 하려면, dispatchcommit의 3번째 전달인자로 { root: true }를 전달하면 됩니다.

modules: {
  foo: {
    namespaced: true,

    getters: {
      // `getters`는 해당 모듈의 지역화된 getters
      // getters의 4번째 인자를 통해서 rootGetters 사용 가능
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 디스패치와 커밋도 해당 모듈의 지역화된 것
      // 전역 디스패치/커밋을 위한 `root` 옵션 설정 가능
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

네임스페이스 모듈에서 전역 action 등록

네임스페이스 모듈에서 전역 action을 등록하려면, root: true를 표시하고 handler 함수에 action을 정의 하면 됩니다.

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

헬퍼에서 네임스페이스 바인딩

헬퍼(mapState, mapGetters, mapActions, mapMutations)를 사용하여 컴포넌트에 네임스페이스 모듈을 바인딩하려고 하면 조금 장황해 질 수 있습니다.

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

위의 코드를 보면 중복된 문자열들을 볼 수 있습니다. 이렇게 코드가 장황해 지는 것을 최소화 하기 위해 헬퍼의 첫 번째 인자에 네임스페이스 경로를 전달 하여 코드를 단순화 할 수 있습니다.

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

또한 createNamespacedHelpers를 사용하여 네임스페이스 헬퍼를 생성할 수도 있습니다. createNamespaceHelper는 전달된 네임스페이스 값으로 바인딩된 새로운 헬퍼를 반환합니다.

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // `some/nested/module`에서 찾음
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // `some/nested/module`에서 찾음
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

플러그인 개발자를 위한 주의사항

개발자가 특정 모듈을 위한 플로그인을 만들고, 사용자가 이 플로그인을 사용한다고 가정했을 때, 개발자가 지정한 네임스페이스와 사용자가 기존에 사용하고 있던 네임스페이스와의 충돌이 발생할 수 있습니다. 이러한 상황을 피하기 위해서 플로그인 옵션을 통해 네임스페이스 값을 전달 받는 것이 좋습니다.

// 플러그인 옵션을 통해 네임스페이스 값 전달
// 그리고 Vuex 플러그인 함수를 반환
export function createPlugin (options = {}) {
  return function (store) {
    // add namespace to plugin module's types
    const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')
  }
}

3. 동적 모듈 등록

store.registerModule 메소드를 사용하여 store가 생성된 후에 모듈을 등록 할 수 있습니다.

store.registerModule('myModule', {
  // ...
})

// `nested/myModule` 중첩 모듈 등록
store.registerModule(['nested', 'myModule'], {
  // ...
})

위의 코드는 store.state.myModulestore.state.nested.myModule로 노출됩니다.

동적 모듈 등록을 사용하면 다른 Vue 플로그인도 어플리케이션의 store에 모듈을 연결하여 상태 관리에 Vuex를 활용할 수 있습니다. 쉽게, 동적 모듈을 사용하면 Vue 플로그인에서도 Vuex를 사용할 수 있게 된다고 이야기 할 수 있습니다, 예를 들어 vuex-router-sync 라이브러리는 vue-router와 vuex를 사용한 라이브러리입니다.

store.unregisterModule(moduleName)을 사용하여 동적으로 등록 된 모듈을 제거할 수도 있습니다. 하지만 이 방법으로는 정적으로 생성된 모듈(store 생성시 선언 되는 모듈)은 제거 할 수 없습니다.

registerModule 사용 할 때, state 보존하기

새 모듈을 등록할 때, 이전 state를 유지해야 한다면, store에 이미 해당 모듈의 state가 포함되어 있어 덮어 쓰지 않아야 한다면, preserveState 옵션을 사용하면 됩니다.
store.registerModule('a', module, { preserveState: true })와 같이 prserveState: true를 하면 모듈이 등록 되고, action, mutation, getter가 store에 추가 되지만, state는 변경 되지 않습니다.

4. 모듈 재사용

동일 모듈을 사용하는 여러 store를 생성하거나, 동일한 모듈을 동일한 store에 여러번 등록 하는 등. 한 모듈에서 여러 인스턴스를 생성해서 사용해야 할 때가 있습니다. state에 일반 객체(예를 들어, state: { ... })를 사용하여 모듈의 state를 선언하면 state 객체가 참조에 의해 공유되기 때문에 state의 오염을 일으킵니다.
예를 들어, A 모듈을 2번 store에 등록 할 경우 (첫번째 A 모듈을 A1으로 두번째 모듈을 A2로 이야기 하겠습니다.) store에 등록한 A1 모듈의 state와 A2 모듈의 state는 완전히 다른 객체인 것 같지만 참조에 의해 공유 됩니다. 즉 어떤 부분(Object, Array...)은 공유하는 객체입니다.
이 문제는 Vue 컴포넌트의 data를 함수로 만들어 객체를 리턴해야 하는 이슈와 완전히 동일한 이슈입니다. 동일한 이슈이기 때문에 역시 해결 방법도 동일합니다. 함수를 사용하여 모듈 state를 선언합니다. (2.3.0+)

const MyReusableModule = {
  state () {
    return {
      foo: 'bar'
    }
  },
  // 변이, 액션, getters...
}

참고

'Vue.JS' 카테고리의 다른 글

[vuex] Strict Mode와 Form Handling  (0) 2019.07.16
[vuex] Plugins  (0) 2019.07.16
[vuex] Modules  (0) 2019.07.16
[vuex] Actions  (0) 2019.07.11
[vuex] Mutations  (0) 2019.07.10
[vuex] Getters  (0) 2019.07.02
댓글
댓글쓰기 폼