Vuex의 Store란 무엇일까?

Vue 개발에서 상태를 관리해 주는 기능을 제공해 주는 것이 Vuex이고 어플리케이션의 모든 컴포넌트들에 대한 중앙 집중식 저장소의 역할 및 관리 를 담당한다.

  • Vuex는 상태관리 패턴 + 라이브러리이며, Vue의 공식 툴이며 ES2015 문법을 기준으로 한다.
  • Promise를 지원하지만, 혹시 지원하지 못하는 브라우저를 생각해야 한다면 es6-promise도 설치해 줘야 한다.

Vuex가 없다면 컴포넌트간의 데이터(상태)를 주고 받기 위해서 부모는 자식에서 props를 통해서 전달하고, 자식은 부모에게 Emit event 방식을 사용해서 처리해야 한다. (이전 게시글의 컴포넌트간의 관계를 참고) 더 큰 문제는 형제 컴포넌트간의 데이터 전달로 EventBus를 사용해야할 정도로 복잡해진다. 이런 문제점들이 간단한 어플리케이션의 경우는 어떻게든 풀어볼 수 있겠지만 대규모 어플리케이션이라면 감당할 수 없을 것이다.

위와 같은 상황들을 해결해 주는 것이 Vuex라고 생각하면 된다. 즉, 데이터를 Store라는 곳을 통해서 관리하고 프로젝트에 존재하는 모든 컴포넌트들이 이 Store를 사용하는 것이다.

Vuex 위치 및 역할
[ 출처 - Vuex공식문서 : Vuex 위치 및 역할 ]

State, Mutations, Actions, Getters

Vuex의 핵심구성 요소들은 State, Mutations, Actions, Getters 들이다.

State (데이터 객체)

  • State는 쉽게 생각하면 공통으로 참조하기 위한 변수를 정의한 것이다.
  • 프로젝트의 모든 곳에서 이를 참조하고 사용할 수 있다.
  • 모든 컴포넌트들에서 공통된 값을 사용할 수 있다.
State 예
export const state = () => ({
  account: null,
  isAdmin: null,
  item: null
})

Mutations (동기형 State 변경 처리기)

  • State 변경을 담당한다. 반드시 Mutation을 통해서만 State를 변경해야 한다.
  • 동기 처리 기준이다.
  • commit('함수명, ‘전달인자’)` 방식으로 호출한다.
  • mutations 내에 함수들을 작성한다.
Mutations 예
export const mutations = {
  currentUser(state, account) {
    state.account = account
  }
}

Actions (Mutation 트리거)

  • Mutation을 실행시키는 역할을 담당한다.
  • 비동기 처리 기준이다.
  • dispatch('함수명', '전달인자') 방식으로 호출한다.
  • Actions 내에 함수들을 작성한다.
  • 비동기 기준이므로 주로 콜백 함수로 작성한다.

일반적인 호출 방식

일반적인 호출 예
// 호출 예
dispatch('setAccount', account);

// actions 정의 in store
export const actions = {
  setAccount({commit, dispatch}, account) {
    commit('currentUser', account);   // Mutation 실행
    dispatch('setIsAdmin', account.uid);
  }
}

컴포넌트에서 콜백 실행 방식

콜백 호출 예
// 호출 예
dispatch('setAccount', account).then(() => {});

// actions 정의 in store
export const actions = {
  setAccount({commit}, account) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('currentUser', account);   // Mutation 실행
        resolve();  // 콜백 처리
      }, 1000)
    })
  }
}

Getters (공통 속성)

  • 각 컴포넌트의 계산된 속성 (computed) 의 공통 속성으로 정의
  • 여러 컴포넌트에서 동일한 computed가 사용될 경우 Getters에 정의해서 공통으로 쉽게 사용 가능
  • 하위 모듈의 Getters를 불러오기 위해서는 this.$store.getters['경로명/함수명'];을 사용 (Store Instance Property 사용)
Getters 예
export const getters = {
  // 현재 로그인 상태 여부 확인 (user 정보 설정 여부로 true/false)
  isAuthenticated(state) {
    return !!state.user;
  },
  // 회원정보 불러오기
  getAccount(state) {
    return state.account;
  }
}

Vuex의 데이터 처리 관계

Vuex의 클래스는 state, mutations, actions, getters 로 구성되며 아래와 같이 구현된다.

Vuex 클래스 구현 예 (store/index.js)
import { Auth, DB, ... } from '@/services/...'

export const strict = false;

/*
** States
*/
export const state = () => ( ... );

/*
** Mutations
*/
export const mutations = { ... };

/*
** Actions
*/
export const actions = { ... };

/*
** Getters
*/
export const actions = { ... };

위의 형태는 모듈별로 구현하기 위한 구조로 각 용도에 따른 Store 구성을 위한 것이다. 예를 들면 User, Product, .... 과 같은 용도별 클래스 구성을 말한다.

Vuex의 처리 흐름은 일반적으로 아래의 그림과 같다.

Vuex Store 데이터 흐름
[ Vuex Store 데이터 흐름 ]

RootState

Vuex 모듈 개발방식에서는 State 변수 값들은 동일 모듈에 있는 State만 참조하도록 제한된다. 만일 다른 모듈 또는 최 상위 State에 대한 활용이 필요한 경우는 해당 State에 직접 접근하는 것이 아니라 rootState를 활용해야 하고 Actions와 Getters의 인자로만 사용 가능하다. 예를 들어 Mutation쪽에서 사용하고 싶은 경우는 Actions에서 인자로 받아서 Mutation쪽으로 commit을 통해서 활용하는 방식으로 처리하게 된다.

Mutations 와 Actions 의 사용 가능 파라미터 규칙

Mutations 함수 파라미터

Mutations 내의 함수에 사용할 수 있는 파라미터들은 (state, payload) 로 한정된다. 즉, 기본적으로 사용할 수 있는 파라미터는 state만 가능하고, Commit을 통해서 전달된 파라미터는 payload만 가능하기 때문에 전달할 때 여러개의 정보가 필요하다면 객체형식으로 전달하면 된다.

Mutations 파라미터
// Mutations fuction signature
funtion_name (state, payload) { .... }  // payload는 commit을 통해서 전달됨
// 여러 정보는 객체 형식으로 처리
function_name (state, { ... }) { ... }

Actions 함수 파라미터

Actions는 기본적으로 비동기 처리를 수행하므로 실행 후 응답이 도착한 순서대로 처리하게 된다. 따라서 파라미터들은 ({ rootState, state, dispatch, commit}, payload) 들로 한정된다. 기본 인자는 객체 형식으로 전달된다. payload도 역시 객체 방식으로 여러 정보를 묶어서 전달할 수 있다.

Actions 파라미터
// Actions fuction signature
funtion_name ({ rootState, state, dispatch, commit }, payload) { .... }
// 여러 정보는 객체 형식으로 처리
function_name ({ rootState, state, dispatch, commit }, { ... }) { ... }

Component에서 Store를 사용하는 방법

State 사용

State에 접근하는 것은 Component의 computed 영역내에서 가능하다.

  • 기본 접근 방법: this.$store.state.items
  • mapState 활용
mapState 활용방법
computed: {
  mapState({
    items: state => state.items   // this.items 속성을 this.$store.state.items 에 매핑
  }),
  ...
}

Mutations 사용

Mutations에 접근하는 것은 Component의 Methods 영역내에서 가능하다.

  • 기본 접근 방법: this.$store.commit('경로명/함수명')
  • mapMutations 활용
mapMutations 활용방법
methods: {
  mapMutations({
    add: 'item/increment'   // this.add() 메서드를 this.$store.commit('item/increment') 에 매핑
  }),
  ...
}

Actions 사용

Actions에 접근하는 것은 Component의 Methods 영역내에서 가능하다.

  • 기본 접근 방법: this.$store.commit('경로명/함수명')
  • mapActions 활용
mapActions 활용방법
methods: {
  mapActions({
    add: 'item/increment'   // this.add() 메서드를 this.$store.dispatch('item/increment') 에 매핑
  }),
  ...
}

Getters 사용

Getters에 접근하는 것은 Component의 computed 영역내에서 가능하다.

  • 기본 접근 방법: this.$store.getters['경로명/함수명']
  • mapGetters 활용
mapGetters 활용방법
computed: {
  mapGetters({
    doneCount: 'item/doneTodosCount'   // this.doneCount 속성을 this.$store.getters['item/doneTodosCount'] 에 매핑
  }),
  ...
}

모듈로 구성된 Vuex에서 상위 모듈에 있는 dispatch, commit 실행 방법

모듈로 구성한 경우는 하위 모듈에서 형제 또는 부모 모듈의 State에 접근하기 위해서는 rootState를 사용하는 것과 같이 Mutations나 Actions를 실행시킬 경우는 3번쨰 파라미터로 {root: true} 를 지정하면 된다.

형제 또는 부모의 Mutations나 Actions 실행하기 위한 Signature
dipatch('paths/function', payload, {root: true});
commit('paths/function, payload, {root: true});

이제 최상위 경로인 root 부터 하위로 경로를 찾아서 처리가 된다.

좀 더 잘 활용할 수 있는 방법은?

검증하면서 걱정이 되었던 부분은 상태 데이터가 커지게 되면 State, Actions, Getters, Mutations 관리가 제대로 가능할지에 대한 것이다. 이런 문제점들과 관련해서 여러 가지 문서들과 샘플들을 확인해 보면 아래와 같이 몇 가지 패턴들을 제공하고 있는 것 같다. 실제 코드를 작성하면서 검증한 것이 아니라 지금까지의 경험에 비해서 추론적으로 판단한 것이기 때문에 실제와 다를 수도 있다.

패턴 적용에 대한 검토

vuex도 react+redux 에서처럼 구조적으로 잘 활용할 수 있도록 다양한 방법과 패턴들을 제공하고 있지만, 오히려 다양함이 더 혼란을 주는것 같고, 뭔가 불필요한 중복 요소들이 생기는 것 같아서 아래와 같이 패턴들을 적용하면서 나름대로 문제점들을 정리하는 방식으로 좀 더 간략하게 사용할 수 있는 방법을 검토해 본다.

Namespace를 활용한 모듈 패턴 적용

관련 문서들을 뒤져보면 namespaced 를 통해서 모듈처리하는 것에 대한 부분이 존재한다.

  • @/store/modules/<module_name> 과 같이 사용할 Store 모듈 경로를 구성한다.
  • 경로 밑으로 state.js, getters.js, mutations.js, actions.js 로 분리해서 파일을 구성한다.
  • 경로 밑의 index.js 에서 namespaced: true 옵션으로 export 처리한다.

문제는 각 기능별로 소스를 분리해 처리하는 것은 좋은데, 변경(네임스페이스, 메서드 명, …)이 발생하면 이에 따른 수정 요소들이 꼬리를 물고 발생할 가능성이 높다.

Binding Helper 패턴 적용

관련 문서를 뒤져보면 mapXXXX 방식으로 Binding Helper를 통해서 처리하는 부분이 존재한다.

  • mapActions, mapState, mapGetters, mapMutations의 바인딩 Helper를 자체적으로 제공한다.
  • namespace가 필요한 경우에는 binding helper에 첫번쨰 아규먼트로 추가해 주면 된다.

이 방식을 사용하면 하나의 파일에서 좀 더 쉽게 코드를 구성할 수는 있을 것 같은데 Helper의 변경이 발생하거나 확장하는데 제한이 발생할 수 있을 것 같다. 문제는 이 패턴에 대한 접근 방법이 일관적이지 않고 아래와 같이 Namepsace를 먼저 만들고 Binding Helper를 적용하는 방법도 존재한다는 점이다. 그리고 네임스페이스, 메서드 명등의 변경으로 발생하는 문제는 똑같이 존재한다.

Namespace를 적용한 Binding Helper 패턴 적용

Vuex에서 제공하는 createNamespaceHelpers를 사용해서 Namespace를 먼저 구성한 후에 mapState, mapMutations, mapActions, mapGetters를 구성하는 방법이다. 사용법은 BindingHelper를 사용하는 방법 그대로 적용할 수 있다.

createNamespaceHelpers 사용 (Store module)
import { createNamespaceHelpers } from 'vuex'
...
const { mapState, mapGetters, mapActions, mapMutations } = createNamespaceHelpers("your_namespace")
...
export { mapState, mapGetters, mapActions, mapMutations }

매번 Binding Helper를 사용할때 지정하던 namespace를 한번만 처리하고 사용하기 때문에 namespace 중복 지정의 문제는 해결이 된 셈이다. 그러나 아직도 문제는 남아 있다. 코드를 작성하다 보면 State/Mutation 에서 중복이 많이 발생하고 Actions/Getters 에서도 거의 중복에 가까운 처리하게 된다.

상수 분리 적용

모듈에서 상수만을 따로 정의하는 코드를 작성하고 이 상수를 사용하는 방식을 추가하면 상수 (메서드 명 등…)를 중복 사용하는 문제를 해결할 수 있다. 물론 수정이 발생해도 한군데서만 적용하면 된다. 나중에는 상수명과 실제 함수명등이 달라지는 것이 문제겠지만…

##결론##

이 문서는 완성된 샘플을 기준으로 한 것이 아니라 검토하면서 계속 진행되고 있는 것이기 때문에 Vuex에 대한 기준 설명은 변동이 없지만 실제 사용하는데 필요한 샘플 코드들을 명확하게 작성하지는 못하고 있다. 위에서 설명했던 모듈 방식의 활용은 vuex-module-decorators 패키지를 이용해서 좀 더 간단하게 구성할 수 있을 듯 하기 때문에 향후 재 작성을 할 예정이다.

참고 자료