본문 바로가기

framework(Vue_Nuxt)

Vuex란?

Vuex란

Vuex란 Vuejs 프레임워크에 사용할 수 있는 상태 관리 패턴이다. Vuex를 사용할 때의 장점은 두가지 이다. 첫번재는 Vuex를 사용하면 해당 어플리케이션의 모든 컴포넌트에 대한 중앙 집중식 저장소 역할을 한다. 예를 들어 SPA(Single Page Application) 방식의 어플리케이션의 경우, 컴퍼넌트 방식으로 잘게 쪼개서 사용한다.

 

 

하나의 어플리케이션, 하나의 화면에는 실제 수많은 컴포넌트로 설계 되어 구성되어져 있다. 이러한 컴포넌트들 간에는 부모 컴포넌트와 자식 컴포넌트의 관계가 존재한다. 우리가 하나 알아야 할 것은 컴포넌트 관계에서 자식 컴포넌트는 부모 컴포넌트의 state를 직접 변경 할 수 없다는 것이다. 만약 자식 컴포넌트에서 부모 컴포넌트의 state를 변경하려면 일단 이벤트 리스터를 자식 컴포넌트에게 전달해준 후, $emit을 통하여 부모 컴포넌트의 함수를 실행 시켜 state를 변경시켜줘야 한다. 이러한 구조 속에서 어플리케이션의 규모가 커져, 부모-자식 컴포넌트의 관계가 많아질 경우 최상위에 부모 컴포넌트의 state의 값을 변경하기 위해 하위의 있는 자식들은 계속해서 인자로 넘겨줘야 한다는 꼬리 물기 식의 불필요한 코드가 반복적으로 생길 것이다. 이러한 부분을 state는 해결해준다.

두번째의 장점은 어플리케이션의 상태에 대해 예측 가능해진다. 필자의 경우 Redux 혹은 Vuex를 사용할 때 가장 중요하게 생각하는 부분이 이 부분이다. 첫번째 이유인 중앙 집중식 저장소 역할을 한다는 것도 중요하지만 예측 가능한 어플리케이션을 만드는 것이 중요한 것 중 하나라고 생각한다. 개인적으로는 어플리케이션의 상태의 흐름만 보고도 해당 도메인 혹은 비지니스에 대한 이해가 어느 정도 가능해야 한다고 생각한다. 이러한 것은 예측 가능성은 Redux 의 경우에는 Redux-devtools 혹은 Vuex의 경우에는 Vue-devtools를 통해 가능하다.

Vuex의 데이터의 흐름을 살펴보면 아래의 사진과 같이 표현할 수 있다.

  • State 는 어플리케이션의 기본적인 Data를 표현한다.
  • Mutations 는 상태의 동기적 변이를 담당하며, Vuex저장소에서 State를 변경할 수 있는 유일한 방법이다.
  • Actions 은 비동기적 작업이 포함되어 있으며, 변이에 대해 Mutations에 commit을 한다.

위의 3가지가 결국 Vuex의 모든 핵심 컨셉이다.

사용자가 Action을 취했을 때, Actions는 Mutations에 commit을 통해 State를 변경한다. 그 후, State의 값이 변경이 되면 해당하는 컴포넌트가 다시 로딩된다. 이 컨셉만 이해해도 거의 전부를 학습했다고 볼 수 있다.

 


Vuex의 저장소 Store

모든 Vuex 어플리케이션에는 전체 어플리케이션의 상태를 보유하고 있는 단일 상태 트리인 store가 있다. 이 store를 Vue 컴포넌트가 바라보고 있으며, 저장소의 상태가 변경되면 효율적으로 대응하고 업데이트 합니다. 이 Store는 직접적으로 변경할 수 없으며, 위에 말했던 Mutations을 통해서만 변경이 가능하여 모든 상태에 대한 추적이 기록에 남길 수 있습니다. 이러한 패턴의 경우 Vuex에만 적용되는 개념이 아닌 Flux라는 개념에서 나온 개념이다. React의 경우 Flux 기반의 Redux를 사용하듯(물론 Redux 외에도 Mobx가 있기는 하지만 이는 여기에서 다룰 내용이 아니기에 언급하지 않는다.), Vue 같은 경우에는 Vuex가 있다.

많은 사람들이 고민하는 것은 Vue로 개발하는 프로젝트는 무조건 Vuex를 도입해야 하냐라는 것이다. 사실 모든 어플리케이션에 Vuex를 필수적으로 도입할 필요는 없다. Vuex를 사용하지 않고도 Vue 컴포넌트의 로컬 데이터를 이용하여 개발하는 데는 문제가 없을 것이다. 다만 프로젝트의 규모가 커지는 경우에는 로컬 데이터만으로는 해결하기 힘든 경우가 있으며, 프로젝트의 규모가 크지 않더라도 비지니스가 복잡하다면 도입하는 것을 추천한다. 비지니스가 복잡한 경우 대체적으로 코드 자체가 복잡해지기 쉽다. 그런 경우는 코드만 보고는 모든 비지니스를 이해하기 힘들 뿐더러 예측 자체가 힘들 수 있다. Vuex를 사용하면 학습에 대한 비용과 로컬 데이터 자체를 루트까지 빼야하는 복잡성이 생길 수 있지만, 그만큼 얻는 이점을 잊지 말아야 한다. 결국에는 Vuex의 도입은 기회비용이라고 생각하는 것이 좋을 것 같다.

 

먼저 Vuex를 사용하기 전에 우리는 Entry 파일에 아래와 같이 컴포넌트에 store 자체에 대한 주입을 해줘야 한다.

// main.js
import { store } from './store/store.js';

new Vue({
    el: '#app',
    store,
    render: h => h(App)
});

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

import state from './states.js';
import getters from './getters.js';
import mutations from './mutations.js';
import actions from './actions.js';

Vue.use(Vuex);

export default new Vuex.Store({
  state,
  getters,
  mutations,
  actions
});


State란 무엇인가?
위와 같이 써주면 우리는 vuex를 사용할 준비가 끝난다. 이제 우리는 state, getters, mutations, actions 파일이 어떻게 생겼는지 하나씩 살펴보도록 하다.

Vuex에는 State라는 하나의 데이터 저장소를 가지고 있다. 어플리케이션의 모든 상태는 하나의 Object 형태로 관리가 된다.

// state.js
const state = {
  todos: []
};

export default state;


위와 같이 정의된 todos의 state를 컴포넌트에서 사용할 때는
 computed를 통해 가져오면 된다. 이는 Vuex 저장소가 반응적이라는 특성 덕분에 가능하다.

 

 

<template>
    <div>{{ getTodo }}</div>
</template>
<script>
  export default {
    // ...
    computed: {
      getTodo () {
        return this.$store.state.todos;
      },
    },
    // ...
  }
</script>

 

 

뭔가의 로컬 state와의 계산이 필요하다면 위와 같이 사용하면 도움이 될 수 있다.

 

<template>
  <div>
    {{ getTodo }}
    {{ doubleTodoCount }}
  </div>
</template>
<script>
  export default {
    // ...
    data () {
      return {
        increasePercent: 15,
      }
    },
    computed: {
      getTodo () {
        return this.$store.state.todos;
      },
      doubleTodoCount () {
        return this.$store.state.todos.length * this.increasePercent
      }
    },
    // ...
  }
</script>

 

하지만 todos 자체만 사용하는 경우라면 mapState 라는 헬퍼 함수를 이용하면 보다 더 간단하게 사용할 수 있다.

 

import { mapState } from 'vuex';
export default {
  // ...
  computed: {
    ...mapState([
      'todos'
    ])
  },
  // ...
}

 

Getters란 무엇인가?

Getters는 쉽게 이야기해서 state를 computed 와 같이 이용할 수 있게 도움주는 함수라고 생각하면 좋다. 물론 위에서 컴포넌트 자체에서 computed를 이용하면 쉽게 이용할 수 있어 문제는 없을 것이다. 하지만 만약 그러한 계산된 값을 여러 곳에서 반복해서 사용해야한다면 해당 코드를 한곳으로 모을 필요성이 있을 것이다. 그럴 때 해당 메소드를 이용하면 좋다.

 

// getters.js
export default {
  getTodosCount (state, getters) {
    return state.todos.length;
  }
}

 

Getter함수의 경우 인자로 state와 getters를 받는다. state의 경우에는 Root state에 접근이 가능하며, 사용하려는 state로 접근하면 된다. 만약 다른 getter와 함께 사용하고 싶다면 다음과 같이 사용할 수 있다.

 

// getters.js
export default {
  getTodosCount (state, getters) {
    return getters.completedCount + state.todos.length;
  }
}

 

이러한 Getter 함수는 컴포넌트에서 state와 마찬가지로 computed 훅에서 사용할 수 있다.

 

<template>
  <div>
    {{ getTodoCount }}
  </div>
</template>
<script>
  export default {
    // ...
    computed: {
      getTodoCount () {
        return this.$store.getters.getTodosCount;
      },
    },
    // ...
  }
</script>

 

찬가지로 해당 컴포넌트와 또다른 로직과 함께 컴포넌트 자체에서만 뭔가의 로직이 들어간다면 위와 같이 사용할 수 있겠지만 그런 것이 아니라면 아래와 같이 mapGetters 메소드를 이용하여 간단하게 사용할 수 있다.

 

import { mapGetters } from 'vuex';
export default {
  // ...
  computed: {
    ...mapGetters([
      'getTodoCount'
    ])
  },
  // ...
}

 

Mutations이란 무엇인가?

Mutations를 번역하면 말 그대로 변이이다. 사실 단어만 들었을 경우는 무슨 말인지 굉장히 헷갈리며, 필자 역시 처음 이 단어를 접하였을 때 이게 무슨 의미인가 싶기도 했다. 간단하게 말하자면 state 자체를 변경하는 메소드이다. 이 Mutations 이라는 것은 Flux 패턴 중 dispatcher 에 해당하는 것이라고 생각하면 된다. Redux의 dispatch가 Vuex에서는 mutations 라고 불리고 있다. Redux와 마찬가지로 Mutation에서는 동기적(Synchronized)이어야 한다.

mutations 함수는 아래와 같이 정의할 수 있다. 해당 함수를 살펴보면 첫번째 인자로는 state를, 두번째 인자로는 payload라고 하는 인자를 받는다. 아래의 경우는 id 라는 string 형태의 인자를 받는다. 사실 이 코드만 보면 이게 무슨 말인지 이해하기가 힘들다. 하지만 함수를 실행시키는 살펴보면 payload라는 인자가 어떤 것인지 쉽게 이해할 수 있다.

 

// mutations.js
const DELETE_MEMO = "DELETE_MEMO";

export default {
  [DELETE_MEMO] (state, id) {
    const targetIndex = state.memos.findIndex(v => v.id === id);
    state.memos.splice(targetIndex, 1);
  },
};

 

위 함수를 실행시키는 방법은 commit을 일으키는 방법이며, 컴포넌트 내에서 실행시키는 방법은 다음과 같다.

 

export default {
  // ...
  methods: {
    test () {
      this.$store.commit('DELETE_TODO', '8785053bf862db8f');
    },
  },
  // ...
}

 

commit의 첫번째 인자로는 mutations의 함수명을, 두번째로는 해당 함수로 넘겨줄 인자를 받는다. mutations DELETE_MEMO 함수의 두번째 인자 id 같은 경우, 실행시킬 때 인자로 던지는 8785053bf862db8f 값에 해당을 하는 것이다. 만약 여러개의 인자를 던지고 싶다면 그럴 경우는 그 파라미터는 순서대로 넣어준 것이 아니라 다음과 같이 object 형태로 인자를 전달해야한다.

 

export default {
  // ...
  methods: {
    test () {
      this.$store.commit('UPDATE_TODO', {
        id: '8785053bf862db8f',
        text: '해당 텍스트로 변경합니다.'
      });
    },
  },
  // ...
}

 

또한 Vuex의 mapMutations 헬퍼를 통해 다음과 같이 사용할 수도 있다.

 

export default {
  // ...
  methods: {
    ...mapMutations([
      UPDATE_TODO,
    ]),
  },
}

 

Actions이란 무엇인가?

아마 Redux를 사용했던 개발자라면 이 Action 부분이 제일 헷갈릴 수 있다. Redux에서 Action은 순수 Plain Object 형태를 유지하며, 해당 type을 Reducer에서는 SubScribe하고 있다가 state를 변경하는데 반면에 Vuex에서는 commit을 통해 Mutations의 이벤트를 실행시킨다. 양쪽을 사용해본 결과 사실 필자의 경우에는 Vuex가 Redux에 비해서는 적은 코드를 유지할 수 있어서 Redux보다는 편하다는 생각이 들었다.

사용하는 방법은 다음과 같다. 앞서와 마찬가지로 삭제하는 함수를 가지고 테스트를 해보겠다.

 

export default {
  deleteTodo ({ commit }, id) {
    axios.delete(`/${id}`)
        .then(() => {
          commit(DELETE_MEMO, id);
        });
  }
}

 

위의 코드와 같이 commit을 이용하여 mutation을 함수를 일으키되, 파라미터는 두번째 인자를 통해 전달하고 있다. 여기에서 눈여겨봐야 할 것은 AJAX를 위한 라이브러리 axios를 action의 함수 안에서 사용하고 있다는 점이다. Action에서는 Mutation의 함수를 커밋할 뿐만 아니라, 비동기에 대한 처리도 일어난다는 것이다. 앞에서 Mutation은 동기적이어야 한다라는 전제가 있었다. Action은 임의의 비동기적인 처리도 가능하다.

이러한 action은 컴포넌트 내에서 다음과 같이 사용할 수 있다.

 

export default {
  // ...
  methods: {
    test () {
      this.$store.dispatch('deleteTodo', '8785053bf862db8f');
    },
  },
  // ...
}

 

위와 같이 실행시키면 8785053bf862db8f 의 인자와 함께 앞서 정의된 deleteTodo 함수명을 가진 action을 실행되며, 실행되는 함수 deleteTodo의 두번째 인자로 id값으로 8785053bf862db8f 인자가 넘겨진다. mutation과 마찬가지로 여러개의 인자를 호출할 떄는 다음과 같이 사용할 수 있다.

 

export default {
  // ...
  methods: {
    test () {
      this.$store.dispatch('updateTodo', {
        id: '8785053bf862db8f',
        text: '해당 텍스트로 변경합니다.'
      });
    },
  },
}

 

또한 Vuex의 mapActions 헬퍼를 통해 다음과 같이 사용할 수도 있다.

 

export default {
  // ...
  methods: {
    ...mapActions([
      'deleteTodo',
      'updateTodo',
    ]),
  },
}