본문 바로가기
VueJS/VueJS

[VueJS] Vuex 사용하기 (2) - 기능 상세 설명 및 다양한 사용법 (ft. Composition API)

by 썸머워즈 2022. 5. 4.
반응형

이 게시글 이전에 [VueJS] Vuex 사용하기 (1) - 설치, 세팅 및 기본 사용법 (ft. Composition API)라는 게시글을 통해 초기 설정을 잡고 가볍게 사용해 보았다.

 

이번 글에서는 Vuex의 Core Concepts에 대해서 알아보도록 하자. 물론 Composition API와 같이 사용할 것이다.


Vuex의 store 내에는 크게 5가지의 속성이 존재하는데

  • state
  • getters
  • mutations
  • actions
  • modules

이렇게 5가지의 속성에 대해 하나씩 알아가 보도록 하자.

 

1. state

Vuex는 단일 상태 트리를 사용하는데, 하나의 애플리케이션은 하나의 store만 가진다는 의미이다.

단일 상태 트리를 사용하면 특정 상태를 쉽게 찾을 수 있으며 디버깅 목적으로 현재 애플리케이션 상태의 스냅숏을 쉽게 만들 수 있다.

 

이전 게시글에서 본 것 처럼 이 store state속성을 통해 여러 컴포넌트에서 공통된 값을 참조할 수 있다.

그냥 쉽게 component에 있는 data와 동일한 역할을 한다 생각하면 된다.

import { createStore } from "vuex";

export default createStore({
    state : {
        counter : 0,
    }
});
import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counter = computed(() => store.state.counter);

    return {counter}
  }
}

 

2. getters

getter는 state를 계산한 값이 필요할 때 사용되는 속성이다.

얼핏 보기로는 computed 와 비슷한 개념을 가지고 있는 것 같아 보인다.

 

그리고 getters는 state를 첫 번째 인수로 받으며, 나머지 인자로는 getters, rootState, rootGetters인데 첫 번째 인수만 필수이고 나머지는 옵션이라 상황에 맞춰 사용해주면 된다.

import { createStore } from "vuex";

export default createStore({
    state : {
        counter : 2,
    },
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    }
});
import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counter = computed(() => store.state.counter);
    const getTwoPowerCounter = computed(() => store.getters.getTwoPowerCounter);

    return {counter, getTwoPowerCounter}
  }
}

 

3. mutations

vuex 저장소에서 실제로 state를 변경하는 유일한 방법은 mutations를 commit 해서 사용하는 것이다.

mutations는 동기적 로직을 정의하는 특징을 가지고 있어 반드시 commit 명령어를 통해 호출해야 한다.

 

그리고 mutations는 methods와 매우 유사한데, state와 전달 인자를 따로 받아서 활용한다.

import { createStore } from "vuex";

export default createStore({
    state : {
        counter : 2,
    },
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
});
import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counter = computed(() => store.state.counter);
    const getTwoPowerCounter = computed(() => store.getters.getTwoPowerCounter);
    const increase = () => store.commit("setCounter", ++counter.value);

    return {counter, getTwoPowerCounter, increase}
  }
}

 

4. actions

mutations와 반대로 비동기적 작업을 할 때 사용된다.

setTimeout()이나 Promise 같은 서버 통신에 주로 사용되며 마지막에 commit을 호출하여 비동기 관리를 한다.

즉, mutations의 메서드를 actions에 commit으로 호출하여 비동기 commit 처리를 한다는 의미이다.

import { createStore } from "vuex";

export default createStore({
    state : {
        counter : 2,
    },
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    commit('setCounter', 2);
                    resolve();
                }, 1000)
            });
        }
    }
});
import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counter = computed(() => store.state.counter);
    const getTwoPowerCounter = computed(() => store.getters.getTwoPowerCounter);
    const increase = () => store.commit("setCounter", ++counter.value);
    const increaseAsync = () => store.dispatch("setCounterAsync");

    return {counter, getTwoPowerCounter, increase, increaseAsync}
  }
}

 

뭐 활용하는 방법에 따라서 async/await를 사용한 처리도 가능하고 비동기가 핵심인 속성이다.

 

5. modules

말 그대로 모듈화 해주는 역할을 하는 속성이다.

저장소의 규모가 커지면 관리가 힘들어지는데, 각각의 state, mutations, getters 등을 포함하는 저장소를 여러 개로 나누고 그거를 나중에 하나로 합치는 역할을 한다.

 

우선 두 모듈을 준비해보자.

 

▷storageA.js

export const storageA = {
    state : () => ({ counter : 3 }),
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            commit('setCounter', 2);
        }
    }
}

 

▷storageB.js

export const storageB = {
    state : () => ({ counter : 10 }),
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            commit('setCounter', 10);
        }
    }
}

 

저 두 개의 모듈을 하나로 합쳐보자.

 

▷index.js

import { createStore } from "vuex";
import { storageA } from "@/store/modules/storageA";
import { storageB } from "@/store/modules/storageB";

export default createStore({
    modules : { storageA, storageB }
});

 

자 그리고 실행하면 ~~~~~ 당연히 에러가 발생한다.

그 이유가 뭘까?

 

우선 에러는 [vuex] duplicate getter key: getTwoPowerCounter라고 정확하게 나타났는데, getter가 겹친다는 것이다.

이게 modules를 사용하게 되면 그 안에 있는 것들을 다 하나로 합치게 되는데,

신기하게도 state는 [state.moduleName.stateName]으로 쪼개져서 들어가는 반면

나머지 getter, mutations, actions는 쪼개져서 들어가지 않고 전역 값으로 들어가게 된다.

const counterA = computed(() => store.state.storageA.counter);

그래서 위 코드처럼 중간에 moduleName을 통해 부를 수가 있다.

 

그렇다면 당연히 겹쳐서 에러가 발생하는 거면 이를 쪼개 주어야 하는데, 그때 사용되는 속성이 namespaced다.

namespaced는 getters, mutations, actions 역시 앞에 moduel 이름을 넣어주는 기능인데 그냥 추가만 해주면 된다.

 

▷storageB.js

export const storageB = {
    namespaced : true,
    state : () => ({ counter : 10 }),
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            commit('setCounter', 10);
        }
    }
}

 

이제 namespaced를 사용하는 방법은 state처럼 호출하는 게 아니라 각각에 따라 다르게 호출한다.

  • state : state.moduleName.stateName으로 호출
  • getters : computed(() => store.getters["moduleName/gettersName"])으로 호출
  • mutations : store.commit("moduleName/mutationsName", params)으로 호출
  • actions : store.dispatch("moduleName/actionsName", params)으로 호출

위 명명규칙을 따라서 호출하게 되면 아래와 같다.

import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counterA = computed(() => store.state.storageA.counter);
    const counterB = computed(() => store.state.storageB.counter);
    const getTwoPowerCounterA = computed(() => store.getters["getTwoPowerCounter"]);
    const getTwoPowerCounterB = computed(() => store.getters["storageB/getTwoPowerCounter"]);
    const increaseA = () => store.commit("setCounter", ++counterA.value);
    const increaseB = () => store.commit("storageB/setCounter", ++counterB.value);
    const increaseAsyncA = () => store.dispatch("setCounterAsync");
    const increaseAsyncB = () => store.dispatch("storageB/setCounterAsync");

    return {counterA, counterB, getTwoPowerCounterA, getTwoPowerCounterB, increaseA, increaseB, increaseAsyncA, increaseAsyncB}
  }
}

 

그리고 아까 namespaced가 설정이 안되어 있으면 전역에 들어간다고 설명을 하였는데,

이 전역에 설정된 actions와 mutations에 다른 component에서 접근이 가능하다.

 

actions에서 사용하는 예제로 마무리 지어보자(mutations 역시 가능하다.)

 

▷storageB.js

export const storageB = {
    namespaced : true,
    state : () => ({ counter : 10 }),
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            commit('setCounter', 10);
        },
        other({dispatch}){
            dispatch("globalAction", null, {root : true});
        }
    }
}

actions에 있는 other 메서드를 보면 3번째 인수에 {root : true}를 전달해주면 된다. (mutations의 경우 역시 마찬가지다.)

 

▷storageA.js

export const storageA = {
    state : () => ({ counter : 3 }),
    getters : {
        getTwoPowerCounter(state){
            return state.counter ** 2;
        }
    },
    mutations : {
        setCounter(state, value){
            state.counter = value;
        }
    },
    actions : {
        setCounterAsync({commit}){
            commit('setCounter', 2);
        },
        globalAction : {
            handler({commit}){
                commit('setCounter', 777);
            }
        }
    }
}

여기서 globalAction에 있는 handler를 통해 storageB에서 호출한 것을 받아서 실행이 가능하다.

 

▷VuexTest.vue

<template>
  <div>
    <h1>Vuex Test with Composition API</h1>
    <div>
        storageA : {{ counterA }}
        {{ getTwoPowerCounterA }}
        <button @click="increaseA">increase</button>
        <button @click="increaseAsyncA">increaseAsync</button>
    </div>
    <div>
        storageB : {{ counterB }}
        {{ getTwoPowerCounterB }}
        <button @click="increaseB">increase</button>
        <button @click="increaseAsyncB">increaseAsync</button>
        <button @click="globalFunc">globalFunc</button>
    </div>
  </div>
</template>

<script>
import { computed } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const counterA = computed(() => store.state.storageA.counter);
    const counterB = computed(() => store.state.storageB.counter);
    const getTwoPowerCounterA = computed(() => store.getters["getTwoPowerCounter"]);
    const getTwoPowerCounterB = computed(() => store.getters["storageB/getTwoPowerCounter"]);
    const increaseA = () => store.commit("setCounter", ++counterA.value);
    const increaseB = () => store.commit("storageB/setCounter", ++counterB.value);
    const increaseAsyncA = () => store.dispatch("setCounterAsync");
    const increaseAsyncB = () => store.dispatch("storageB/setCounterAsync");

    // storageA globalFunc
    const globalFunc = () => store.dispatch("storageB/other");

    return {
            counterA, counterB, getTwoPowerCounterA, getTwoPowerCounterB, 
            increaseA, increaseB, increaseAsyncA, increaseAsyncB,
            globalFunc
        }
  }
}
</script>

<style>
</style>

 

▶ 결과

 

정리하다 보니 코드가 굉장히 지저분해졌다...


추가적으로 helper역할을 하는 mapState, mapGetters, mapMutations, mapActions 가 존재하는데, 찾아보니까 vue3에서는 그다지 필요 없는 녀석들이라는 거 같아 굳이 정리는 하지 않았다.

아래 참고 글에 같이 정리해둔 게시글이 있으니 필요하면 가서 보도록 하자.


참고 : https://vuex.vuejs.org/guide/state.html#single-state-tree

 

State | Vuex

State Single State Tree Vuex uses a single state tree - that is, this single object contains all your application level state and serves as the "single source of truth." This also means usually you will have only one store for each application. A single st

vuex.vuejs.org

참고 : Vuex-개념과-예제-이해하기​

 

Vuex란? 개념과 예제

📢 들어가기 전에 이번 포스팅에선 Vuex가 무엇인지 알아보고, 간단한 예제를 구현해본다. Vue CLI로 설치한 Vue.js 프로젝트 환경에서 진행했다. Vuex를 시작하기 전에, 아래 방법으로 Vue.js 프로젝트

doozi0316.tistory.com

참고 : vue3에서 vuex 사용

 

vue3에서 vuex 사용법, vue, computed, reactive, ref, watch, watchEffect, props, vuex, composable, module, composition-api

vue3에서 vuex 사용법, vue, computed, reactive, ref, watch, watchEffect, props, vuex, composable, module, composition-api

kyounghwan01.github.io

 

반응형


댓글

TOP