Vue3에서 Vuex 사용해보기

김도겸

Updated:


Intro

안녕하세요. 김도겸입니다.
Vue의 상태관리 라이브러리인 Vuex의 개념과 구조 및 사용법에 대해 스터디한 것을 정리해보았습니다.



1. Vuex란???

Vue에서 data()에 해당하는 것, 즉, 컴포넌트 간 공유할 수 있는 데이터들을 상태(state)라고 부릅니다.
이 데이터들을 컴포넌트에 공유하기 위해서 props와 emit을 활용하지만 점점 규모가 커질수록 작은 단위로 쪼개진 여러개의 컴포넌트로 화면을 구성하고, 페이지 수가 늘어나며 데이터를 주고 받는 컴포넌트 간의 관계는 점점 복잡해질 수 밖에 없습니다.
이와 같은 문제점을 해결하기 위해 데이터 전달을 유기적으로 관리할 목적으로 Vue에서 지원하는 것이 여러 컴포넌트 간의 데이터 전달과 이벤트 통신을 한 곳에서 관리하는 패턴이자 라이브러리인 Vuex입니다.


2. Vuex의 구조

  • Vuex의 속성
    Vuex는 state, mutaitions, action, getters 4가지의 형태로 관리가 되며, 단방향 데이터 흐름으로 처리하고, store라는 이름을 가진 파일로 관리하므로 통상 store라고 불립니다.

    • state
      state는 Vue 컴포넌트에서 data로 볼 수 있고, 원본 소스의 역할을 하며, View와 직접적으로 연결되어있는 Model입니다.
      이 state는 직접적인 변경이 불가능하고 mutation을 통해서만 변경이 가능하다. mutation을 통해 state가 변경이 일어나면 반응적으로 view가 업데이트됩니다.

    • mutaitions
      mutation은 state를 변경하는 유일한 방법이고 이벤트와 유사합니다.
      mutation은 함수로 구현되며 첫 번째 인자는 state를 받을 수 있으며, 두 번째 인자는 payload를 받을 수 있습니다.
      여기서 payload는 여러 필드를 포함할 수 있는 객체 형태도 가능합니다. 일반적으로 직접 호출은 할 수 없으며, commit을 통해서만 호출할 수 있습니다.

      실무에서는 api를 통해 전달 받은 데이터를 가공하여 state를 설정하는 데 주로 사용됩니다.

    • actions
      action은 mutation과 비슷하지만 비동기 작업이 가능하다는 차이점이 있습니다.
      또한 mutation에 대한 commit 호출이 가능하여 action에서도 mutation을 통해 state를 변경할 수 있습니다.
      action에서는 첫 번째 인자를 context 인자로 받을 수 있으며 이 context에는 state, commit, dispatch, rootstate와 같은 속성들을 포함하고, 두번째 인자는 mutation과 동일하게 payload로 받을 수 있습니다.
      action은 dispach를 통해 호출이 가능하고, action에서는 서로 다른 action을 호출 할 수 있습니다.

      실무에서 actions는 Axios를 통한 API 호출과 그 결과에 대해서 반환을 하거나 mutation으로 commit하여 상태를 변경하는 용도로 주로 사용됩니다.

    • getters
      getters는 Vue 컴포넌트에서 Computed의 역할을 해준다고 볼 수 있습니다.
      특정 state에 대해 어떠한 연산을 하고, 그 결과를 View에 바인딩할 수 있으며, state의 변경 여부에 따라 getter는 재계산이 되고 View 역시 업데이트가 이루어집니다.
      이때 state는 원본데이터로서 변경이 일어나지 않습니다.

      실무에서 state의 반복적인 연산 처리가 필요한 내용에 대해 getters를 사용합니다.

  • Vuex flow
    vuex diagram
    Vuex를 사용하면 위 다이어그램과 같은 flow로 구성됩니다.

    1. vue component에서 action 메서드 실행(dispatch)
    2. action을 통해 비동기 로직 처리 후에 Mutation 메서드 실행(commit)
    3. Mutation에서 데이터를 가공하여 state를 변경함
    4. 변경된 state를 vue component에 렌더링해줌.


3. 설치 및 사용법

  • 설치 방법
    Vue 프로젝트에 Vuex는 다음과 같이 설치하면 됩니다. (Vue3 기준) npm install vuex

  • main.js에 store 세팅

      import { createApp } from "vue";
      import App from "./App.vue";
      import store from "./store";
    
      createApp(App)
      .use(store)
      .mount("#app");
    
  • store 폴더 생성 후 index.js 파일 생성

      import { createStore } from "vuex";
    
      export default createStore({
          state: {}, 
          getters: {},
          mutations: {},
          actions: {},
      });
    
  • 4가지 속성 기본적인 사용법 news api를 가져오는 프로젝트라고 가정했을 때 예제 코드입니다.

    • state

      사용할 data를 다음과 같이 정의합니다.

      state:{
          news: []
      },
      

      이를 component에서 사용할 때에는 다음과 같이 사용합니다.

      setup(){
          const newsState = computed(() => store.state.news);
      }
      

      컴포넌트 에서 사용하려면 computed로 묶어야만 state의 data 값이 바뀌는 것을 감지하고 반응하여 렌더링해줍니다.

    • getters

      사용할 getter를 다음과 같이 정의합니다.

      getters: getFirstNewsTitle(state){
          return state.newsState[0].title;
      },
      

      해당 코드는 state의 data인 newsState list의 가장 첫 아이템의 title을 리턴해줍니다.
      최근순으로 따졌을 때 가장 최근 뉴스의 제목이 필요할때 사용하면 좋을 것 같습니다.

      setup(){
          const firstNewsTitle = computed(() => store.getters.getFirstNewsTitle);
          console.log(firstNewsTitle.value);
      
          //getter가 여러 개 필요할 때는
          const newsGetters = computed(() => store.getters);
          console.log(newsGetters.value.GETTER_NAME);
      }
      

      컴포넌트에서 사용할 경우엔, state와 마찬가지로 computed로 묶어 사용해야 합니다.

    • mutations

      mutation을 다음과 같이 정의하여 사용합니다.

      mutations:{
          setNews(state, news) {
              //동기적 로직 정의 가능
              state.news = news
          }
      },
      

      다음과 같이 간단한 setter처럼 구현합니다.
      (setter처럼 구현하지 않고 다른 동기적 로직도 정의 가능합니다.)

      setup() {
          function resetData() {
              store.commit("setNews", []);
          } 
      }
      

      주로 실무에선 actions에서 호출하지만 컴포넌트에서 호출할 경우엔 위와 같이 호출하면 됩니다. (굳이 사용한다면 다음과 같이 간단히 리스트를 초기화해주는 정도로 사용 가능할 것 같습니다.)

    • actions

      actions는 다음과 같이 정의하여 사용합니다.

      import { fetchNewsList } from "../api/index"
      ...
      actions:{
          getNewsData(context) {
              //fetchNewsList라는 GET API 호출 메서드가 있다고 가정함.
              fetchNewsList()
                  .then(response => {
                      // mutation 호출
                      context.commit('setNews', response.data);
                  })
                  .catch(error => {
                      console.log(error)
                  })         
          }
      },
      

      위와 같이 api 호출 등의 비동기 로직을 actions에서 정의하여 사용합니다.
      주로 비동기 로직 처리 후에 mutation을 호출의 state의 데이터를 변경해주는 코드까지 작성합니다.

      setup() {
          function newsData() {
              store.dispatch("getNewsData");
          } 
      }
      

      컴포넌트에서 호출할 경우에 위와 같이 사용하면 됩니다. (function으로 묶지않고 setup 생명주기에 바로 실행시켜도 상관없습니다.)


4. store를 분리하기

실무에선 위 예제처럼 하나의 store 파일로는 관리할 수 없기 때문에 상황에 따라 적절히 분리하여 사용해야 합니다.
분리하는 방법이 2가지가 있어 나눠서 설명드리도록 하겠습니다.

  • 동일한 데이터 속성 별로 모듈로 나누는 방법

    프로젝트의 각각의 기능에 맞춰 여러 개 만들어 세분화하는 방식입니다.

      const moduleA = {
          namespaced: true,
          state: () => ({ ... }),
          mutations: { ... },
          actions: { ... },
          getters: { ... }
      }
    
      const moduleB = {
          namespaced: true,
          state: () => ({ ... }),
          mutations: { ... },
          actions: { ... }
      }
    
      const store = createStore({
          modules: {
              a: moduleA,
              b: moduleB
          }
      })
    

    만약 먼저 간단하게 한 파일 내에서 모듈을 분리한다면 위와 같은 예제로 분리할 수 있습니다.
    이러한 방식과 같이 기본적인 분리 방법을 알면 파일별로 분리하는 것도 간단합니다.

    namespaced를 true로 설정해야 store module 간의 중복적인 이름을 가진 속성을 사용할 때 일어나는 충돌을 막을 수 있습니다.

      // store/moduleA.js
      export const moduleA = {
          namespaced: true,
          state: () => ({ ... }),
          mutations: { ... },
          actions: { ... },
          getters: { ... }
      }
    
    
      // store/moduleB.js
      export const moduleB = {
          namespaced: true,
          state: () => ({ ... }),
          mutations: { ... },
          actions: { ... }
      }
    
      // store/index.js
      import { moduleA } from './store/moduleA.js';
      import { moduleB } from './store/moduleB.js';
      const store = createStore({
          modules: {
              a: moduleA,
              b: moduleB
          }
      })
    
      export default store;
    
    

    위에 modules에서 선언한 모듈명을 아래와 같이 component에서 사용합니다.

        
      ...
    
      setup() {
          const store = useStore();
    
          const state = computed(() => store.state.moduleName.stateName); 
          const getter = computed(() => store.getters["moduleName/getterName]);
            
          store.commit("moduleName/mutationName", params)
          store.dispatch("moduleName/actionName", params)
      }
    

    위와 같이 컴포넌트에서 사용하면 됩니다.

    ※ rootState, rootGetters ※
    rootState와 rootGetters는 getters나 actions의 전달인자에서 볼 수 있습니다.
    이는 루트 저장소 즉, 모듈화를 했을 때 modules를 선언한 store파일의 state와 getters를 불러올 수 있습니다.

    위 예제 코드를 좀 변경해서 설명드리겠습니다.

      // store/index.js
      import { moduleA } from './store/moduleA.js';
      import { moduleB } from './store/moduleB.js';
      const store = createStore({
          state: { test: 1},
          getters: { getTest: state => state.test++},
          modules: {
              a: moduleA,
              b: moduleB
          }
      })
    
      export default store;
    
    

    store/index.js 파일에 state와 getter를 위와 같이 추가하고,

      // store/moduleA.js
      export const moduleA = {
          namespaced: true,
          state: () => ({ ... }),
          mutations: { ... },
          actions: {
              logRootData(context, payload) {
                  console.log(context.rootState.test) // 1
                  console.log(context.rootGetters.getTest) // 2
              }
          },
          getters: { 
              getRootState (state, getters, rootState) {
                  return rootState.test
              },
              getRootGetters (state, getters, rootState, rootGetters) {
                  return rootGetters.getTest
              }, 
          }
      }
    
    

    store/moduleA.js 파일에서 다음과 같이 전달인자를 통해 사용할 수 있습니다.

    이를 응용한다면 다른 모듈에 존재하는 state나 getter를 root store에 등록하여 사용할 수 있기 때문에 상황에 따라 유용한 기능이라고 생각합니다.

  • store의 각 속성 별로 모듈로 나누는 방법

    store를 state, getters, mutations, actions 각각의 파일로 분리하는 방식입니다.

      // store/state.js
      const state = {
          todoList: [
              {id:1, todo: "영화보기", done: false},
              {id:2, todo: "게임하기", done: true},
              {id:3, todo: "공부하기", done: false},
          ]
      };
    
      export default state;
    
      // store/getters.js
      const getters = {
          getList: state => state.todoList
      };
    
      export default getters;
    
      // store/mutations.js
      const mutations = {
          setList: (state, payload) => {
              ....
          }
      };
      export default mutations;
    
      // store/actions.js
      const actions =  {
          getListData: (context, payload) => {
             ....
          }
      };
    
      export default actions;
    
    
    
       // store/index.js
      import stateModule from './state.js';
      import gettersModule from './getters.js';
      import mutationsModule from './mutations.js';
      import actionsModule from './actions.js';
    
      const store = createStore({
          state: stateModule,
          getters: gettersModule,
          mutations: mutationsModule,
          actions: actionsModule
      });
    
    

    컴포넌트에서의 사용방법은 동일합니다.

    여기서는 기본적인 틀과 방법만 정리하였지만, 모듈화를 익히시고, 능숙하게 사용하신다면, 더욱 깔끔하고 다양하게 모듈로 분리하실 수 있습니다.


※ Vuex Helpers ※

Vuex Helpers는 options-api 방식일 때 컴포넌트 내에서 vuex를 사용할 경우에 반복되는 코드를 줄이고 코드의 효율성 및 재활용 성을 높이는 방법입니다. (composition-api에서는 helpers를 사용하는 방법이 복잡하여 외부 라이브러리를 활용해야 합니다.)

state, getters, mutations, actions 각각 helpers로 변경해서 사용하는 기본적인 방법을 예제를 통해 보여드리겠습니다.

import { mapState, mapActions, mapMutations, mapGetters } from 'vuex'

export default {
  name: 'Sample',
  computed: {
    //1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 state 입력
    ...mapState('book', {
        msg: state => state.message     // -> this.msg
    }),
    //1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 getter 입력
    ...mapGetters('book', [
       'getMsg'       // -> this.getMsg
    ])
  },
  methods: {
    //1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 mutation 입력
    ...mapMutations('book', [
        'changeMessage'     // -> this.changeMessage()
    ]),
    //1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 action 입력
    ...mapActions('book', [
        'callMutation'      // -> this.callMutation()
    ])
  }
}

위 코드의 옆에 각각 주석을 달아놓은 거처럼 this.{name}으로 사용하고, component에서 사용할땐 this.를 빼고 사용합니다. 단일 store 파일만 쓰는 경우에는 1번째 전달인자를 사용하지 않으면 됩니다.

createNamespacedHelpers를 사용하여 store 모듈을 미리 설정할 수 있습니다.

import { createNamespacedHelpers } from 'vuex'

const bookHelper = createNamespacedHelpers('book');
const bookListHelper = createNamespacedHelpers('bookList');

export default {
  name: 'Sample',
  computed: {
    ...bookHelper.mapState({
        message: state => state.message     // -> this.message
    }),
    ...bookHelper.mapGetters([
       'getMsg'       // -> this.getMsg
    ]),
    ...bookListHelper.mapState({
        messageList: state => state.messageList     // -> this.messageList
    }),
    ...bookListHelper.mapGetters([
       'getMsgList'       // -> this.getMsgList
    ])
  },
  methods: {
    ...bookHelper.mapMutations([
        'changeMessage'     // -> this.changeMessage()
    ]),
    ...bookHelper.mapActions([
        'callMutation'      // -> this.callMutation()
    ]),
    ...bookListHelper.mapMutations([
        'changeMessageList'     // -> this.changeMessageList()
    ]),
    ...bookListHelper.mapActions([
        'callMutationList'      // -> this.callMutationList()
    ]),
  }
}

Tags:

Categories:

Updated:

Leave a comment