IT/언어

[Vue.js] Vuex를 가볍게 배워보자

개발자 두더지 2023. 4. 13. 22:14
728x90

Vuex란?


vue.js의 기본을 알고있다는 전제에 한해서 설명하도록 하겠다.

 수 많은 컴포넌트가 있는 어느 정도 규모가 있는 프로젝트가 있다고 가정하자. 이 그림에서 ComponentA의 데이터를 ComponentC나 ComponentE에 전달하고자 한다면 어떻게 할까?

 일반적인 vue.js라면 부모 컴포넌트에서 자식 컴포넌트로, 다시 자식 컴포넌트에서 부모 컴포넌트로의 데이터를 전달하는 는 과정을 여러번 걸쳐야한다.

 여기서 Vuex를 사용하면 글로벌 변수와 같은 느낌으로 데이터를 다룰 수 있게 된다. 이것으로 복잡했던 컴포넌트 간의 데이터 송신 과정을 줄일 수 있다.

 

 

Vuex의 도입


 Vuex의 패키지를 설치한다. vue cli으로 프로젝트를 생성하면 세트로 설치되므로 이러한 경우에는 필요하지 않다.

npm install vuex

 Vuex용의 파일을 새롭게 마련한다.

 파일명은 어떻게 해도 좋으나, store.js으로 하는 것이 일반적이다. 

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex); // vuex를vue전체에 사용하도록 선언

export default new Vuex.Store({ // main.js으로 읽어들이도록 한다.
 // 아래에 정의한 것은 어떤 컴포넌트에도 사용 가능하게 된다.
  state: { 
    number: 2
  }
})

 

 

Vuex의 개념


 Vuex에는 다음과 같은 다섯 가지 개념이 있다. [state], [mutations], [getters], [actions], [modules]가 바로 그것이다. 각각의 개념에 대해서 설명하도록 하겠다.

1. state

  • store에서 관리하고 있는 상태(공통 변수, 데이터). 컴포넌트의 data와 같이 데이터를 보존하고 있는 장소.
  • getters로 참고되어, 갱신은 mutations로 한다.

 store에서 state를 획득하는 가장 심플한 방법은 산출 속성(computed)을 사용하여 반환하도록 하는 것이다. 직접 "store.state.count"로 store 상태를 갖도록 할 수 있지만, 이렇게 하면  모듈을 사용했을 때 store의 상태를 사용하고 있는 모든 컴포넌트를 import해야한다는 결점이 생긴다.

//Vuex Store(공통 부분)
const store = new Vuex.Store({
  state: {
    count: 0
  }
});


//Vue Component(컴포넌트1, 가게)
Vue.component('my-component', {
  data() {
    return {
    };
  },
  computed: {
    //Vuex store의 count를 감시하여 획득한다.
    count() {
      return store.state.count;
    }
  },
  template: `
    <div>
       <p>{{ count }}</p>
    </div>
  `,
});


new Vue({
  el: '#app'
});

 위와 같은 문제를 해결하기 위해서는 루트 컴포넌트에 store 옵션을 지정하면 된다. 지정 방법은 다음과 같다.

new Vue({
  el: '#app',
   // 루트 인스턴스에 store옵션을 전달한다.
   // this.$store로 각 컴포넌트에 참조할 수 있게 된다.
   store,
});

 전체 코드로 본다면 다음과 같다.

//Vuex Store(공통 부분)
const store = new Vuex.Store({
  state: {
    count: 0
  }
});


//Vue Component(컴포넌트1, 가게)
Vue.component('my-component', {
  data() {
    return {
    };
  },
  computed: {
    //Vuex store의 count를 감시하여 획득한다.
    count() {
      return store.state.count;
    }
  },
  template: `
    <div>
       <p>{{ count }}</p>
    </div>
  `,
});


new Vue({
  el: '#app',
   // 루트 인스턴스에 store옵션을 전달한다.
   // this.$store로 각 컴포넌트에 참조할 수 있게 된다.
   store,
});

 또 다른 해결 방법으로는 mapState 헬퍼를 사용하는 것이다 산출 속성(Computed)로 선언 방법을 개선하는 방법이다. 아래의 코드를 보면 알 수 있듯, 산출 속성을 모두 선언하는 것은 확장이다.

 이것은 산출 속성을 선언하던 원래의 방법이다.

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  count2 () {
      return this.$store.state.count2
    }
  count3 () {
      return this.$store.state.count3
    }
   ...
  }
}

 다음은 mapState를 활용하여 개선하는 방법이다.

computed: mapState({
    count:  'count',  // count: state => state.count와 같다.
    count2: 'count2', // count2: state => state.count2와 같다.
    count3: 'count3'  // count3: state => state.count3와 같다.
  })

 물론 import하는 것도 잊지 말길!

import {mapState} from 'vuex'

 

2. getters

  • state 상태를 바탕으로 산출된 값을 반환하는 함수가 정의된 장소
  • state의 데이터를 가공하여 표시
  • state 가공하므로, 첫 번째 인수는 state
  • 상태를 필터링, 카운트한 값을 반환
//Vuex Store(공통창고)
const store = new Vuex.Store({
  state: {
    //★ store상태:todos를 선턴
    todos: [
      { id: 1, text: 'A', done: true },
      { id: 1, text: 'B', done: true },
      { id: 2, text: 'C', done: false }
    ]
  },
  //★getters은 store상태를 획득하여 가공하는 함수를 쓰는 장소
  getters: {
    //★getter선언:doneTodos를 선언
    //filter으로 todo.done가 true의 데이터를 획득
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    //★getter함수:doneTodosCount를 선언
    //length으로 위의 getters.doneTodos의 데이터를 계산
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    }
  }
});


//Vue Component(컴포넌트1、가게)
Vue.component('my-component', {
  data() {
    return {
    };
  },
  store,
  computed: {
    //Vuex store의 count를 감시하여 획득
    todo_count() {
       //★【this.$store.getters】로 store에 getter의doneTodosCoun반환값을 획득
      return this.$store.getters.doneTodosCount;
    }
  },
  template: `
    <div>
        <!-- 계산 속성으로 count함수의 반환값을 획득하여 표시 -->
       <p>{{ todo_count }}</p>
    </div>
  `,
});


new Vue({
  el: '#app',
   // 루트 인스턴스에 store옵션을 전달
   //★이렇게 하여, this.$store로 각 컴포넌트에서 참조할 수 있게 된다.
   store,
});

View Compiled

Resources

 

3. mutations

  • state를 갱신하는 함수가 적혀 있는 장소
  • state의 갱신은 하지 않음
  • 첫 번째 인수는 반드시 state, 그 이후의 인수는 payload
  • state 상태를 갱신할 때는 반드시 commit을 사용
// 숫자를 입력하여 버튼을 누르면,
// 입력된 값과 토탈이 합산된다.
//Vuex Store(공통 창고)
const store = new Vuex.Store({
  state: {
    total: 0,
  },
  mutations: {
    //★여기에 state상태를 변경하는 함수를 준비
    increment (state, plus) {
      state.total += plus
    }
  },  
});


//Vue Component(컴포넌트1, 가게)
Vue.component('my-component', {
  //★ 내부 데이터 plus
  data() {
    return {
      plus: 0,
    };
  },
  methods: {
    increment(plus) {
      // ★Vuex store의 total를 갱신할 때, commit을 사용
      // Vuex store의 내장 함수인 increment로 state상태를 갱신
      // 함수increment은 인수가 필요하므로, this.$store.commit(함수, 인수)
      this.$store.commit('increment', plus);
    }
  },
  computed: {
    //Vuex store의 total를 감시하고 획득
    //computed에서는 감시하는 값이 변경됐을 경우에 total()가 실행된다.
    //npm이나 yarn로 설치한 사람은 ”mapState”를 사용해보길 바란다.
    total() {
      return this.$store.state.total;
    }
  },
  template: `
    <div class="m-3">
    <p>Please enter the number you want to add!</p>
       <p>You add {{ plus }}</p>
       <input v-model.number="plus" type="number"></input><br>
       <button v-on:click="increment(plus)" class="btn btn-danger flex-item mt-2">"Clcik Me"</button>
      <p> {{ total }} </p>
    </div>
  `,
});


new Vue({
  el: '#app',
   //★ 이렇게 하는 것으로, this.$store으로 각 컴포넌트에서 참조할 수 있게 된다.
  store,
});

View Compiled

Resources

 

4. actions

  • 비동기 처리나 외부 API 통신하는 장소
  • action으로 비동기 처리를 시작 > 실제 state의 갱신은 mutations를 commit으로 실행 > action으로 state의 갱신하지 않고, 최종적으로 mutations으로 데이터를 커밋하며, commit은 동기여야한다.

actions 처리

 아래의 예로 actions의 처리 과정을 살펴보자. 일단 전체 처리 흐름에 대해 설명하자면, 컴포너틑에서 dispatch로 액션을 호출하여 액션 내에서 외부 API등을 통해 비동기 처리를 한 후에 commit으로 뮤테이션을 사용해 스테이트를 갱신하는 흐름이 된다.

 먼저, 컴포넌트내에서 methods에서 Actions을 dispatch로 실행한다.

methods: {
    increment(plus) {
   //plus는 인수
      this.$store.dispatch('incrementAsync', plus);
      this.$store.dispatch('warningAsync');
    }
  },

 그 후에 store의 Actions로 commit을 호출한다.

  actions:{
    incrementAsync({ commit }, plus) {
      setTimeout(()=>{
        alert("이것이 비동기처리이다.");
        commit('increment', plus)
      }, 5000) //비동기로 5초정도 처리를 늦추는 것이 가능하다.
    },
    warningAsync({ commit }) {    
        commit('warning')
    }
  },

 그리고 commit으로 Mutations에서 state를 변경한다.

 mutations: {
    //★여기서 state상태를 변경하는 함수를 준비한다.
    increment (state, plus) {
      state.total += plus
      state.warning_show = false
    },
    warning (state) {
      state.warning = "5초 기다려주세요."
      state.warning_show = true
    }
  },

API 통신

  • 이번에는 비동기 API 통신으로 라이브러리 axio를 이용한다.
  • async 함수를 사용해본다(필수는 아니다).
//Vuex Store(공통 창고)
const store = new Vuex.Store({
  state: {
    results: null,
  },
  mutations: {
    //★여기에 state상태를 변경하는 함수를 준비한다.
    getResults_mutation (state, results) {
      state.results = results
    }
  },
  actions:{
     async getResults({ commit }) {
     await axios
           .get('https://api.coindesk.com/v1/bpi/currentprice.json')
           .then(response =>  commit("getResults_mutation", response));
    },
  }
});


//Vue Component(컴포넌트1、가게)
Vue.component('my-component', {
  //★ 내부 데이터 plus
  data() {
    return {
    };
  },
  methods: {
    getRresults() {
       this.$store.dispatch('getResults');
    }
  },
  computed: {
     results() {
      return this.$store.state.results;
    }
  },
  mounted(){
       axios
      .get('https://api.coindesk.com/v1/bpi/currentprice.json')
      .then(response => (this.re = response))
  },
  template: `
    <div>
    <p>Click the button and show the api results</p>
       <button v-on:click="getRresults()" class="btn btn-danger flex-item mt-2">"Clcik Me"</button>
       <p> {{ results }} </p>

    </div>
  `,
});


new Vue({
  el: '#app',
   //★ 이것으로 this.$store롤 각 컴포넌트에서 참조할 수 있게 된다.
  store,
});

 

5. modules

  • 위 네 가지 store 구성 요소를 분할한 것이다.
  • 어플리케이션의 거대화로 인해 거대해진 store에 대해 보기 좋도록 모듈을 분할한다.

 조금 더 modules에 대해 조금 더 풀어서 설명하자면, 개발에 따라 코드가 거대화되므로 몇 백개의 state, getters, mutaions, actions를 하나의 파일로 작성하면 관리하기 어렵기 때문에 사용하는 방법이다. 

 그럼 모듈을 사용해보자.

 첫 번째 방법은 한 파일에서 모듈을 나누는 방법이다. 예를 들면 다음과 같다.

// 모듈A
const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

// 모듈B
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    module_a: moduleA,
    module_b: moduleB
  }
})

store.state.module_a // -> `moduleA` 의 store
store.state.module_b // -> `moduleB` 의 store

 두 번째 방법은 모듈을 여러 파일로 나눠서 사용하는 방법이다.

 이 방법은 먼저 "superFunction"과 "header"를 store에 등록한다. 

import Vue from "vue";
import Vuex from "vuex";

import superFunction from "./superFunction";
import header from "./header";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    superFunction,
    header
  }
});

export default store;

 그 다음은 모듈의 지정을 명확히 하기 위해 namespaced: true를 잊지 않고 해준다. namespaced: 옵션을 true로 하면, 각각의 모듈에 namespaced를 부여해 호출하는 방식을 관리할 수 있게 된다.

 그러나 state에 대해서는 namepaced:ture와 관계없이 일괄적으로 $store.state.(모듈명).data.message과 같이 호출한다. 그 외의 mutation, action, getter은 namespaced:true를 부여하지 않은 경우 모듈을 사용하지 않고 글로벌로 등록했을 때와 같이 호출한다.

 혹시 namespaced : true가 부여되어 있지 않은 여러 개의 모듈 내에서 이름이 덮어쓰여진 경우, mutation과 action은 각각 동시에 실행된다. 그리고 getter은 다음과 같은 에러를 발생한다.

[vuex] duplicate getter key: greetingC.

 namespced: ture가 부여된 mutation, action,getter은 이름 앞에 모듈명을 붙여서 호출하는 것으로, 위의 에러를 방지할 수 있다.

console.log(this.$store.getters['moduleA/greeting']
console.log(this.$store.commit('moduleA/greeting')
console.log(this.$store.dispatch('moduleA/greeting')

 전체 코드로 보면 다음과 같다.

const state = {
  appNumber: 0
};

const getters = {
  appNumber(state) {
    return state.appNumber;
  }
};

const actions = {
  changeNumber({ commit }, val) {
    commit("changeNumber", val);
  }
};

const mutations = {
  changeNumber(state, value) {
    state.appNumber = state.appNumber + value;
  }
};

const superFunction = {
  namespaced: true, // 잊지말고
  state,
  getters,
  actions,
  mutations
};

export default superFunction; // 모듈의 이름

 마지막으로 컴포넌트로 모듈 : superFunction을 사용한다.

 그러나 주의해야할 점은 getter과 action의 참조 방식이 바뀐다는 것이다.

  • this.$store.getters["moduleName/getterName"]
  • this.$store.dispatch("moduleName/actionName")
<template>
  <main>
    {{appNumber}}
    <Controller :changeNumber="changeNumber"/>
  </main>
</template>

<script>
import Controller from "./Controller";
export default {
  components: {
    Controller
  },
  computed: {
    appNumber() {
      return this.$store.getters["superFunction/appNumber"];
    }
  },
  methods: {
    changeNumber(val) {
      this.$store.dispatch("superFunction/changeNumber", val);
    }
  }
};
</script>

 

참고자료

https://qiita.com/moyegogo1020/items/0b471e08a227a26cb31b

https://qiita.com/shin_moto/items/d1ef189e74253ac692ef

728x90