본문 바로가기
Dev/✅ Vue.js

Vuex 모듈 및 사용 방법 정리

by 아아덕후 2024. 2. 12.
반응형

Intro

현재 Vue.js에서 공식적으로 지원하는 상태관리 라이브러리는 Vuex에서 Pinia로 변경되었지만,
업무에서 Vuex를 사용하고 있어 이를 정리하기 위해 작성한 글입니다.
(그래도.. 아직.. Vuex 사용.. 하시는 분들.. 많으시죠..?)
 
저는 하나의 View 안에 여러 탭 간 이동을 구현해야 하는 업무가 있었습니다.
탭 간(레이아웃 단위 컴포넌트) 이동 시 라우터 이동과 라이프사이클에 따라 초기화되는 vue 인스턴스 data의 문제를 해결하고
각 탭의 상태 관리하기 위해 Vuex를 사용했었습니다.
 
이를 공유하고 다른 분 들께서도 조금 더 쉽게 Vuex를 적용하시는데 도움을 드리고 싶어 글을 작성했습니다.
 

(Pinia에서는 Vuex에서보다 더 간단한 Composition-API 스타일의 API를 제공하며,  TypeScript 함께 사용하는데 유용하다고 합니다.)
 


목차

  1. Vuex 설치 ( Vite + Vue.js )
  2. Vuex store 디렉토리 생성 및 main.js 설정 추가
  3. Vuex 사용을 위한 index.js 생성 및 모듈 설정
  4. 초기화되는 Vue 인스턴스 데이터 
  5. Vue 컴포넌트에서 Vuex 불러오기
  6. Vuex 적용 예제
  • 참고

 


 기본 설정을 마치셨다면 6번에서 실행 예제 코드를 바로 보시면 됩니다!

1. Vuex 설치 ( Vite + Vue.js )

Vuex 설치에 앞서 저는 vite + vue.js 환경을 구성했습니다.
(vite에 대한 글 : 2023.03.27 - [Dev/🟨 JavaScript] - [Vite.js] 빛처럼 빠른 빁 ⚡ ( with. Vue 설치 ))
 
node 환경에서 아래 명령어로 최신버전 vite를 설치할 수 있습니다.

npm create vite@latest

 
 

vite + vue 설치

 
vtie + vue.js 설치를 했다면 이제 (구) 상태관리 라이브러리 Vuex를 설치하겠습니다.
설치 이전에 생성한 프로젝트로 이동! 해주시는 것 잊으시면 안 됩니다.
(저처럼 root 폴더에 설치해 버릴 수 있으니까요! )

// NPM
npm install vuex@next --save

// Yarn
yarn add vuex@next --save

 

설치결과

 
커맨드로 Vuex까지 설치하면 파일 폴더 구조는 아래처럼 프로젝트가 생성되고 node modules까지 설치된 상황일 것 같습니다.

 
프로젝트 생성했으니 한번 볼까요? 
 

VVV

 
(+ 참고) 
저는 페이지 이동 ( vue 인스턴스 mount , unmount ) 시에도 유지되는 Vuex state를 테스트하기 위해서 vue-router도 설치 후 작업했습니다. 
참고 : 2023.05.02 - [Dev/✅ Vue.js] - [Vue3] vue-router 설치부터 환경설정 및 사용방법
 


 

2. Vuex store 디렉토리 생성 및 main.js 설정 추가

다음으로 상태관리 라이브러리 Vuex store 폴더를 생성하고 main.js설정을 해주어야 합니다.
 
디렉토리 최상단에 있는 src 폴더의 내부로 이동해서 store 폴더를 생성합니다.
그리고 store 폴더 내에 index.js 파일을 생성해 줍니다.
이 친구가 html / index.js처럼 Vuex의 메인 Welcome Page 역할을 해줍니다.

프로젝트 폴더    
   ├─── node_modules
   ├─── ...
   └─── src
               ├─── assets
               ├─── components
               ├─── ...
               ├─── store ( 여기예요! )
               │           └─── index.js
               └───  App.vue

 

store 폴더 및 index.js 생성

 
store 폴더 생성을 완료했다면, Vue 프로젝트의 main.js에 해당 store를 추가해주어야 합니다.
 
 

// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; 	// vue-router 추가
import store from "./store";  	// Vuex 추가

createApp(App)
	.use(store)
	.use(router)
	.mount("#app");

 
main.js 파일에 위에서 설치한 vue-router 및 Vuex의 store 폴더를 import 해오고
.use() 메서드를 통해 연결시켜야 합니다!
 


 

3. Vuex 사용을 위한 index.js 생성 및 모듈 설정

store 폴더 및 index.js 생성 후, vue파일에서 사용할 vuex의 state, mutations 등에 대해서 설정을 작성합니다.
 
해당 index 파일로 모듈(store 하위에 각 업무 혹은 기능에 따라 폴더로 관리) 별로 관리하는 root로 사용할 수 있습니다.

// index.js
import { createStore } from "vuex";

export default createStore({
  namespaced: true,
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {},
});

 
index.js 파일에 위와 같이 세팅을 해줍니다.
 
저의 경우 모듈 기능을 사용하기 위해서
각 모듈 Vuex 파일들을 index.js에서 import 하여 index.js를 기준으로 사용했습니다.
모듈 파일들은 index.js파일과는 다르게 위와 같이 createStore사용하지 않고 아래와 같이 템플릿을 사용했습니다.

// module vuex
const state = {};
const mutations = {};
const actions = {};
const getters = {};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters,
};

 
 
 
모듈 기능을 사용한 파일 구조는 아래와 같습니다.

 
저는 store 폴더 내 index.js를 위치시키고,
같은 레벨에 espresso, latte 모듈 폴더를 만들고 각 레이아웃(컴포넌트)에 매핑시킬 store js 파일을 생성했습니다.
 
그리고 아래와 같이 index.js에서 각 Vuex js 파일을 import 하여 moudules에 담아주었습니다.

import { createStore } from "vuex";
import module_americano from "./espresso/americano.js";
import module_dolce from "./latte/dolce.js";

export default createStore({
  namespaced: true,
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    module_americano,
    module_dolce,
  },
});

 
해당 구조를 다이어그램으로 표현하자면
Vuex (store 폴더)의 espresso , latte 폴더에 있는 각 컴포넌트에 매핑하여 사용할 store 파일(americano.js, dolce.js)을 index.js의 modules에 import해오도록 설계했습니다.
 

Vuex - store 폴더구조

 
 
index.js 파일에서 import 해오는 americano.js, dolce.js 파일은 아래와 같습니다.
 

 


 

4. 초기화되는 Vue 인스턴스 데이터 

 
다음으로는 Vue 파일에서 Vuex 사용하는 방법입니다.
Vue 파일로 되어있는 컴포넌트는 router 이동에 따라
이동하는 컴포넌트는 mount, 이동 전 컴포넌트는 unmount 됩니다.
 
아래 PageA.vue, PageB.vue 컴포넌트에서 선언한 vue 인스턴스 데이터는 마운트 해제될 때 아래와 같이 초기화됩니다.
 

// PageA.vue

<script>
export default {
  data() {
    return {
      pageTitle: "Page A",
      instanceDataOfPageA: 72,
    };
  },
  methods: {
    onClickAddNum() {
      this.instanceDataOfPageA++;
    },
    onClickMinusNum() {
      this.instanceDataOfPageA--;
    },
  },
};
</script>

<template>
  <div class="pageA-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page A 인스턴스 변수</h4>
      <ul>
        <li>
          별점 <span class="desc">(default : 72)</span> :
          {{ instanceDataOfPageA }}
        </li>
      </ul>
      <button class="btn" type="button" @click="onClickAddNum()">증가</button>
      <button class="btn" type="button" @click="onClickMinusNum()">감소</button>
    </div>
  </div>
</template>

<style scoped>
.pageA-border {
  border: 0.5px solid #c59e9e;
  padding: 10px;
  margin: 10px;
}
.btn {
  margin-left: 10px;
}
.desc {
  color: grey;
  font-size: 9pt;
}
.img-box {
  background-image: url("../../assets/img/americano.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
</style>

 

// PageB.vue
<script>
export default {
  data() {
    return {
      pageTitle: "Page B",
      instanceDataOfPageB: 35,
    };
  },
  methods: {
    onClickAddNum() {
      this.instanceDataOfPageB++;
    },
    onClickMinusNum() {
      this.instanceDataOfPageB--;
    },
  },
};
</script>

<template>
  <div class="pageB-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page B 인스턴스 변수</h4>

      <ul>
        <li>
          별점<span class="desc">(default :35)</span> :
          {{ instanceDataOfPageB }}
        </li>
      </ul>
      <button class="btn" type="button" @click="onClickAddNum()">증가</button>
      <button class="btn" type="button" @click="onClickMinusNum()">감소</button>
    </div>
  </div>
</template>

<style scoped>
.pageB-border {
  border: 0.5px solid #499784;
  padding: 10px;
  margin: 10px;
}
.img-box {
  background-image: url("../../assets/img/latte.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
.desc {
  color: grey;
  font-size: 9pt;
}

.btn {
  margin-left: 10px;
}
</style>

 
 

라우터 이동에 따른 vue 인스턴스 데이터 초기화

 
 
page A에서 default 값으로 별점을 72점으로 선언 후 증가/감소로 70으로 만들었지만,
page B로 이동 후 page A로 다시 복귀했을 때는 70이 아닌 처음 선언 값인 72로 초기화되어 있습니다.
 
이를 탭(라우터 이동) 간 데이터를 유지하기 위해 vuex를 통해 문제를 해결해 보겠습니다.


 

5. Vue 컴포넌트에서 Vuex 불러오기

실제 들어가기에 앞서 Vuex 파일 구조 및 설정에 대해 간단하게 설명드리면, 
 

State

  • 여러 컴포넌트 간에 공유되는 데이터
  • computed에서 호출

Getters

  • 상태(state) 값이 변경되었을 때 변화에 따른 차이를 자동으로 반영하여 자동으로 값을 계산
  • computed에서 호출

Mutations

  • 뮤테이션의 개념은 methods 속성과 매칭
  • 상태 값을 변경하는 유일한 방법 state는 mutations로만 변경하기!
  • methods에서 호출

Actions

  • 액션(actions)은 뮤테이션 중에서 비동기 처리 로직들을 정의하는 속성
  • 동기 처리는 뮤테이션, 비동기 처리는 액션
  • methods에서 호출

 
정리하면, 화면(컴포넌트) → 화면에서의 통신/이벤트 발생(Actions/Mutation) 데이터 변경(State)으로 흐름이 특징입니다.
제 업무에서는 통신(Axios 이용)은 Actions에서 요청/응답을 받은 후 
Mutation을 호출하여 State에 응답 값을 담아주었습니다.
(데이터의 흐름 Actions Mutations State)

 

Vue에서 Vuex Import 하기

 
Vue 컴포넌트에서 위에서 선언한 Vuex를 사용하기 위해서는
아래와 같이 Vuex에서 제공하는 mapState, mapGetters 등의 메서드를 import 하여 사용합니다.

<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";

export default {
	...
    computed: {
        ...mapState([ "A", "B" ])
        ...mapGetters([ "A1", "B1"])
	},
    methods :{
        ...mapActions([ "C" , "D" ])
        ...mapMutations([ "C1" , "D1" ])
    },
    ...
}
</script>

 

6. Vuex 적용 예제

제가 소개드릴 Vuex를 사용하는 방법은 Component Binding Helpers입니다.
위에서 index.js의 modules에 선언한 store 파일들을 아래 Vuex의 메서드를 import하여 사용했습니다.

  • createNamespacedHelpers
  • mapState, mapGetters, mapMutations, mapActions

우선 PageA 컴포넌트는 ameriacno.js를 사용하였고
PageB 컴포넌트에서는 dolce.js를 사용해 아래와 같이 화면을 구성했습니다.
 

6-1 state 설정

 
두 개의 각 화면에서는

  • vue 컴포넌트의 인스턴스 변수
  • americano.js / dolce.js에서 선언한 state
  • index.js에서 pageA , pageB에서 공통으로 사용하는 state

3개의 변수를 출력했습니다.
첫 번째 인스턴스 변수는 라우터 이동에 따라 컴포넌트가 unmount 되며 초기화되지만, 
아래 두 개의 Vuex를 이요한 state는 라우터 이동에도 초기화 되지 않습니다.
이를 이용해 각 컴포넌트에서 필요한 고유의 값들은 컴포넌트에 매핑된 ( pageA : americano.js / PageB : dolce.js ) state를 이용하고 
공통으로 사용하는 state는 index.js에서 선언한 state를 사용하도록 아래와 같이 구성했습니다.
 

index.js : 여러 화면/컴포넌트에서 공통으로 사용하는 state
각 화면/컴포넌트에서만 사용하는 개별 state ( 좌: americano.js / 우 : dolce.js)

 
 
이를 각 컴포넌트에서 사용하기 위한 코드는 아래와 같습니다.
<script> 부분의 import , americanoHelper / dolceHelpercomputed 메서드mapState를 이용했습니다.

// pageA.vue
<script>
import { mapState, mapGetters, mapMutations, createNamespacedHelpers } from "vuex";

const americanoHelper = createNamespacedHelpers("module_americano");

export default {
  data() {
    return {
      pageTitle: "Americano Page A",
      instanceDataOfPageA: 72,
    };
  },
  computed: {
    // module_americano.js의 state
    ...americanoHelper.mapState(["icedAmericano"]),
    
    // index.js의 state
    ...mapState(["COMMON_COFFEE"]),
    
    // index.js의 getters
    ...mapGetters(["COMMON_totalCoffeeCount", "COMMON_totalCoffeePrice"]),
  },
};
</script>

<template>
  <div class="pageA-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page A 인스턴스 변수</h4>
      <ul>
        <li>
          별점 <span class="desc">(default : 72)</span> :
          {{ instanceDataOfPageA }}
        </li>
      </ul>
    </div>
    <div label="vuex-data">
      <h4>page A Vuex state (americano.js)</h4>
      <ul>
        <li>{{ icedAmericano.name }} 가격 : {{ icedAmericano.price }}</li>
      </ul>
    </div>
    <div label="vuex-data-advance">
      <h4>page A, Page B Vuex 공통 state (index.js)</h4>
      <ul v-for="(coffee, idx) in COMMON_COFFEE" :key="`coffee${idx}`">
        <li>{{ coffee.name }} : {{ coffee.count }}개</li>
      </ul>
      <ul>
        <li>총 {{ COMMON_totalCoffeeCount }}개</li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.pageA-border {
  border: 0.5px solid #c59e9e;
  padding: 10px;
  margin: 10px;
}
.btn {
  margin-left: 10px;
}
.desc {
  color: grey;
  font-size: 9pt;
}

.img-box {
  background-image: url("../../assets/img/americano.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
</style>

 

// pageB.vue
<script>
import {
  mapState,
  mapGetters,
  mapMutations,
  createNamespacedHelpers,
} from "vuex";

const dolceHelper = createNamespacedHelpers("module_dolce");

export default {
  data() {
    return {
      pageTitle: "Dolce Page B",
      instanceDataOfPageB: 35,
    };
  },
  computed: {
    // module_dolce.js의 state
    ...dolceHelper.mapState(["dolceLatte"]),

    // index.js의 state
    ...mapState(["COMMON_COFFEE"]),

    // index.js의 getters
    ...mapGetters(["COMMON_totalCoffeeCount", "COMMON_totalCoffeePrice"]),
  },
};
</script>

<template>
  <div class="pageB-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page B 인스턴스 변수</h4>
      <ul>
        <li>
          별점<span class="desc">(default :35)</span> :
          {{ instanceDataOfPageB }}
        </li>
      </ul>
    </div>
    <div label="vuex-data">
      <h4>page B Vuex state (dolce.js)</h4>
      <ul>
        <li>{{ dolceLatte.name }} 가격: {{ dolceLatte.price }}</li>
      </ul>
    </div>
    <div label="vuex-data-advance">
      <h4>page A, Page B Vuex 공통 state (index.js)</h4>
      <ul v-for="(coffee, idx) in COMMON_COFFEE" :key="`coffee${idx}`">
        <li>{{ coffee.name }} : {{ coffee.count }}개</li>
      </ul>
      <ul>
        <li>총 {{ COMMON_totalCoffeeCount }}개</li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.pageB-border {
  border: 0.5px solid #499784;
  padding: 10px;
  margin: 10px;
}
.img-box {
  background-image: url("../../assets/img/latte.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
.desc {
  color: grey;
  font-size: 9pt;
}

.btn {
  margin-left: 10px;
}
</style>


 
 
[추가] mapState를 이용해서 state 불러오는 여러 방법

    // module_americano.js의 state 불러오는 3가지 방법
    ...mapState("module_americano", ["icedAmericano"]),
    ...americanoHelper.mapState(["icedAmericano"]),
    ...createNamespacedHelpers("module_americano").mapState(["icedAmericano"]),
    
    
    // state 변수명을 변경해서 불러오기 (호출하는 template 에서 변경된 key 값으로 사용)
    ...mapState("module_americano", {
      icedAmericanoNaming: (state) => state.icedAmericano,
    }),
    ...americanoHelper.mapState({
      icedAmericanoNaming: (state) => state.icedAmericano,
    }),
    ...createNamespacedHelpers("module_americano").mapState({
      icedAmericanoNaming: (state) => state.icedAmericano,
    }),

공통적으로 mapState의 첫 번째 인자로 index.js의 modules에 선언한 모듈명을 입력하고 해당 모듈에서 가져 올 state 명을 배열로 작성하면 createNamespacedHelpers를 사용한 것과 같이 사용이 가능합니다.
그리고 위에서 createNamespacedHelpers를 축약하지 않고 computed 메서드에서 직접 호출 후 모듈명을 입력하면 동일하게 사용 가능합니다.
 
그리고 마지막으로 각 모듈에서 지정한 state명을 mapState에서 문자열 배열이 아닌 객체로 변경하여 key값을 지정하면
변경한 key 값으로 해당 컴포넌트에서 사용가능합니다.
 

6-2 mutations 및 getters 설정

다음은 위에서 선언한 state 값을 변경하기 위한 mutation을 호출하는 예제 코드입니다.
해당 mutation을 사용하여 라우터 이동에도 유지되는 state 값을 관찰할 수 있습니다.
 

초기화되는 vue 인스턴스 데이터 / 유지되는 vuex state 데이터

 
그리고 여기서는 mutation으로 변경한 state의 값을 자동으로 계산하는 getters(맨 아래 총 개수)를 사용했습니다.
이를 통해 각 컴포넌트에서 변경한 금액 (각 모듈 js )과 공통으로 사용하는 개수 (index.js) 값 변경을 확인할 수 있습니다.
 

index.js : 여러 화면/컴포넌트에서 공통으로 사용하는 state의 값을 변경하는 mutation, state를 계산하는 getters

 

각 화면/컴포넌트에서만 사용하는 개별 state를 변경하는 mutations ( 좌: americano.js / 우 : dolce.js)

 
 
이를 각 컴포넌트에서 사용하기 위한 코드는 아래와 같습니다.
<script> 부분의 import , americanoHelper / dolceHelper computed에 mapGetters(mapState와 동일) , 그리고 methods의mapMutations으로 호출합니다.
 
 

// pageA.vue
<script>
import {
  mapState,
  mapGetters,
  mapMutations,
  createNamespacedHelpers,
} from "vuex";
import store from "/src/store";

const americanoHelper = createNamespacedHelpers("module_americano");

export default {
  data() {
    return {
      pageTitle: "Americano Page A",
      instanceDataOfPageA: 72,
    };
  },
  computed: {
    // index.js의 state
    ...mapState(["COMMON_COFFEE"]),

    // module_americano.js의 state
    ...americanoHelper.mapState(["icedAmericano"]),

    // index.js의 getters
    ...mapGetters(["COMMON_totalCoffeeCount", "COMMON_totalCoffeePrice"]),
  },
  methods: {
    ...mapMutations([
      "COMMON_incrementCoffeeCount",
      "COMMON_decrementCoffeeCount",
    ]),

    // module_americano.js의 mutations 불러오는 3가지 방법
    ...americanoHelper.mapMutations([
      "incrementIcedAmericanoPrice",
      "decrementIcedAmericanoPrice",
    ]),
    // ...createNamespacedHelpers("module_americano").mapMutations([
    //   "incrementIcedAmericanoPrice",
    //   "decrementIcedAmericanoPrice",
    // ]),
    // ...mapMutations("module_americano", [
    //   "incrementIcedAmericanoPrice",
    //   "decrementIcedAmericanoPrice",
    // ]),

    onClickAddNum() {
      this.instanceDataOfPageA++;
    },
    onClickMinusNum() {
      this.instanceDataOfPageA--;
    },
    onClickAddIcedAmericano() {},
  },
};
</script>

<template>
  <div class="pageA-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page A 인스턴스 변수</h4>
      <ul>
        <li>
          별점 <span class="desc">(default : 72)</span> :
          {{ instanceDataOfPageA }}
        </li>
      </ul>
      <button class="btn" type="button" @click="onClickAddNum()">증가</button>
      <button class="btn" type="button" @click="onClickMinusNum()">감소</button>
    </div>
    <div label="vuex-data">
      <h4>page A Vuex state (americano.js)</h4>
      <ul>
        <li>
          {{ icedAmericano.name }} 가격 :
          {{ icedAmericano.price }}
        </li>
      </ul>
      <button class="btn" type="button" @click="incrementIcedAmericanoPrice()">
        증액
      </button>
      <button class="btn" type="button" @click="decrementIcedAmericanoPrice()">
        감액
      </button>
    </div>
    <div label="vuex-data-advance">
      <h4>page A, Page B Vuex 공통 state (index.js)</h4>
      <ul v-for="(coffee, idx) in COMMON_COFFEE" :key="`coffee${idx}`">
        <li>{{ coffee.name }} : {{ coffee.count }}개</li>
      </ul>
      <ul>
        <li>총 {{ COMMON_totalCoffeeCount }}개</li>
      </ul>
      <button class="btn" type="button" @click="COMMON_incrementCoffeeCount(0)">
        아아추가
      </button>
      <button class="btn" type="button" @click="COMMON_decrementCoffeeCount(0)">
        아아감소
      </button>
    </div>
  </div>
</template>

<style scoped>
.pageA-border {
  border: 0.5px solid #c59e9e;
  padding: 10px;
  margin: 10px;
}
.btn {
  margin-left: 10px;
}
.desc {
  color: grey;
  font-size: 9pt;
}

.img-box {
  background-image: url("../../assets/img/americano.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
</style>

 

// pageB.vue
<script>
import {
  mapState,
  mapGetters,
  mapMutations,
  createNamespacedHelpers,
} from "vuex";

const dolceHelper = createNamespacedHelpers("module_dolce");

export default {
  data() {
    return {
      pageTitle: "Dolce Page B",
      instanceDataOfPageB: 35,
    };
  },
  computed: {
    // module_dolce.js의 state
    ...dolceHelper.mapState(["dolceLatte"]),

    // index.js의 state
    ...mapState(["COMMON_COFFEE"]),

    // index.js의 getters
    ...mapGetters(["COMMON_totalCoffeeCount", "COMMON_totalCoffeePrice"]),
  },
  methods: {
    ...mapMutations([
      "COMMON_incrementCoffeeCount",
      "COMMON_decrementCoffeeCount",
    ]),

    ...dolceHelper.mapMutations([
      "incrementDolceLattePrice",
      "decrementDolceLattePrice",
    ]),
    onClickAddNum() {
      this.instanceDataOfPageB++;
    },
    onClickMinusNum() {
      this.instanceDataOfPageB--;
    },
  },
};
</script>

<template>
  <div class="pageB-border">
    <h2>{{ pageTitle }}</h2>
    <div class="img-box"></div>
    <div label="instance-data">
      <h4>page B 인스턴스 변수</h4>

      <ul>
        <li>
          별점<span class="desc">(default :35)</span> :
          {{ instanceDataOfPageB }}
        </li>
      </ul>
      <button class="btn" type="button" @click="onClickAddNum()">증가</button>
      <button class="btn" type="button" @click="onClickMinusNum()">감소</button>
    </div>
    <div label="vuex-data">
      <h4>page B Vuex state (dolce.js)</h4>
      <ul>
        <li>{{ dolceLatte.name }} 가격: {{ dolceLatte.price }}</li>
      </ul>
      <button class="btn" type="button" @click="incrementDolceLattePrice()">
        증액
      </button>
      <button class="btn" type="button" @click="decrementDolceLattePrice()">
        감액
      </button>
    </div>
    <div label="vuex-data-advance">
      <h4>page A, Page B Vuex 공통 state (index.js)</h4>
      <ul v-for="(coffee, idx) in COMMON_COFFEE" :key="`coffee${idx}`">
        <li>{{ coffee.name }} : {{ coffee.count }}개</li>
      </ul>
      <ul>
        <li>총 {{ COMMON_totalCoffeeCount }}개</li>
      </ul>
      <button class="btn" type="button" @click="COMMON_incrementCoffeeCount(1)">
        돌체라떼 추가
      </button>
      <button class="btn" type="button" @click="COMMON_decrementCoffeeCount(1)">
        돌체라떼 감소
      </button>
    </div>
  </div>
</template>

<style scoped>
.pageB-border {
  border: 0.5px solid #499784;
  padding: 10px;
  margin: 10px;
}
.img-box {
  background-image: url("../../assets/img/latte.jpg");
  background-size: cover;
  width: 400px;
  height: 400px;
}
.desc {
  color: grey;
  font-size: 9pt;
}

.btn {
  margin-left: 10px;
}
</style>

 
 
[추가] mapMutations를 호출하는 3가지 방법

 // module_americano.js의 mutations 불러오는 3가지 방법
...americanoHelper.mapMutations([
  "incrementIcedAmericanoPrice",
  "decrementIcedAmericanoPrice",
]),

...createNamespacedHelpers("module_americano").mapMutations([
  "incrementIcedAmericanoPrice",
  "decrementIcedAmericanoPrice",
]),

...mapMutations("module_americano", [
  "incrementIcedAmericanoPrice",
  "decrementIcedAmericanoPrice",
]),

위 mapState를 호출할 때와 마찬가지로 mutaions에서도 동일하게 적용 가능합니다.
 
 

6-3 actions 설정

마지막으로 actions를 이용한 통신 예제 코드입니다.
저는 actions에서 http 통신 (axios 이용) 후 응답 값을 받아 기존에 선언해 놓은 state를 mutation으로 변경하는 프로세스를 사용했습니다. 
( actions -> mutation -> state 변경)
이때 사용한 통신 예제는 reqres.in이라는 api를 사용하여 통신 테스트를 진행했습니다.
해당 api에서 아바타 데이터를 제공하기 때문에 바리스타 조회하도록 기능을 추가했습니다.
vue에서 사용하는 통신 라이브러리인 axios에 대한 예제는 아래 글을 참고해 주시면 감사하겠습니다.
2023.04.18 - [Dev/✅ Vue.js] - [Vue] Axios를 이용한 http 통신 - 설치부터 예제, 비동기 처리까지.
 
 

action를 이용한 통신 및 데이터 변화(state) 출력

 
아래 코드에서 actions의 getAmericanoBaristar 함수를 통해 API 통신을 합니다.
해당 통신이 성공하면 체인메서드로 then에서 응답 값을 받아 commit 메서드(vuex에서 mutation 호출 시 사용하는 함수)를 통해 첫 번째 인자에 mutation명인 setAmericanoBaristar를, 두번째 인자에는 통신에서 받아온 응답 값을 입력합니다.
이를 통해 응답 값을 해당 state인 americanoBarista에 저장합니다.
 

// americano.js
const actions = {
  getAmericanoBaristar({ commit }, params) {
    axios
      .get(`https://reqres.in/api/users/${params}`)
      .then((res) => {
        // 통신 성공했을 경우, mutation으로 state 응답 값으로 변경
        commit("setAmericanoBaristar", res.data.data);
      })
      .catch((res) => {
        // 실패했을 경우
        console.error("실패 ", res);
      });
}

 

// americano.js
const mutations = {
  setAmericanoBaristar(state, payload) {
    state.americanoBarista = payload;
  },
}

const state ={
  americanoBarista: {},
}

 
아래는 pageA.vue 의 수정된 부분만 따온 코드입니다.
helper를 이용해서 computed에서 state를, methods에서 actions를 import하고 
 
vue 화면에서 바리스타 조회 버튼 클릭 시,
-> [vue] onClickGetBaristar() 함수 실행
-> [vue] 함수에서 1~10 랜덤 수 뽑아서 action에 전달
-> [js] 전달받은 파라미터를 id로 action에서 http 통신 (js) 아바타 조회
-> [js] mutation으로 응답 값을 state 변경
-> [vue] 화면에서 변경 된 state 값 확인
의 순서로 해당 바리스타를 조회, state 변경 후 화면에 출력합니다.

// pageA.vue
<script>
import { createNamespacedHelpers } from "vuex";
const americanoHelper = createNamespacedHelpers("module_americano");

export default {
  computed: {
    ...americanoHelper.mapState(["americanoBarista"]),
  },
  methods: {
    ...americanoHelper.mapActions(["getAmericanoBaristar"]),
      
    onClickGetBaristar() {
      let baristarId = Math.floor(Math.random() * 10 + 1);
      this.getAmericanoBaristar(baristarId);
    },
  }
}
</script>

<template>
    <div class="vuex-action">
      <h4>
        page A 아메리카노 바리스타 조회 (Vuex actions 통신) (americano.js)
      </h4>
      <template v-if="americanoBarista?.avatar">
        <ul>
          <span>
            <img :src="americanoBarista?.avatar" alt="avatar" />
          </span>
          <li class="avatar-desc">
            name : {{ americanoBarista?.first_name }} {{ americanoBarista?.last_name }}
          </li>
          <li>email : {{ americanoBarista?.email }}</li>
        </ul></template
      >
      <button class="btn" type="button" @click="onClickGetBaristar()">
        바리스타 조회
      </button>
    </div>
</template>

 


 
간단하게나마 Vuex설치 및 모듈설정부터 state, getters, mutations, 그리고 actions까지 간단한 예제를 통해 사용하는 글을 작성했습니다.
처음 프로젝트 세팅 시 동일하게 템플릿 및 컨벤션을 작성한다면 개발 시 동료들과 협업에 더 유용할 것이라고 생각해서 글을 작성했습니다.
해당 코드는 https://github.com/heonsooo/toy-box/tree/main/soo-vue-vite-boilerplate 에서 확인하실 수 있습니다.
 
이 글이 Vuex 사용에 조금이라도 도움이 되셨다면 좋겠습니다.
혹시 수정해야 할 부분 있으시면 댓글로 남겨주시면 감사하겠습니다.
 
감사합니다!
 
 


참고

반응형

댓글