Vuex란?

Vue.js의 상태 관리를 위한 라이브러리, 모든 컴포넌트에서 접근 가능한 중앙 집중식 데이터 저장소이다.

상태 관리? 왜 상태 관리가 필요할까?

컴포넌트 기반 프레임워크에서는 작은 단위로 쪼개진 여러개의 컴포넌트로 화면을 구성하는데

컴포넌트가 많아질수록 컴포넌트간의 통신이나 데이터를 전달하는 과정이 복잡해지고, 흐름을 파악하는 것이 어려워진다.

(props로 데이터를 전달하는 경우에는 여러개의 컴포넌트를 거쳐야 하고 이벤트버스를 이용하면 컴포넌트 구조가 복잡해질수록 이벤트가 어디서 발생했는지 일일이 찾기가 번거로울 것이다)

이에 따라 데이터 전달과 통신을 한 곳에서 관리하여 과정을 단순화 하고 데이터 흐름을 쉽게 파악하기 위해 상태관리가 필요하다.

 

Vuex를 사용하기 위해서는 먼저 프로젝트에 Vuex를 설치해야 한다.

터미널에서 설치 명령어를 실행한다.

npm install vuex

그리고 Vuex를 등록할 자바스크립트 파일을 하나 새로 생성한다. (보통 src > Store 경로를 만들어 Store 폴더에 저장한다)

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  // 내용 작성하는 부분
});

(이번 글에서 나오는 Vuex를 적용하는 모든 코드는 Vue 2 버전을 기준으로 하고 있습니다. Vue 3 버전과는 차이가 있기 때문에 3 버전은 다르게 작성해야 합니다 - 추후에 추가 예정)

 

이제 프로젝트의 main.js 파일로 가서 store.js를 import 하고 등록해주면 된다.

// main.js
import Vue from "vue";
import App from "./App.vue";
// store.js를 불러오는 코드
import { store } from "./store";

new Vue({
  el: "#app",
  // 뷰 인스턴스의 store 속성에 연결
  store: store,
  render: h => h(App)
});

등록을 마쳤으니 이제 store.js에 내용을 작성해본다.

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  // counter라는 state 속성을 추가
  state: {
    counter: 0
  }
});

state에 정의된 counter 속성은 컴포넌트의 data 속성에 counter를 정의한 것과 같은 역할을 한다.

특정 컴포넌트 안에서만 사용할 수 있던 data 속성을 전역에서 사용할 수 있게 된 것이라고 생각하면 된다.

 

이렇게 등록한 state의 counter 속성은 어느 컴포넌트에서든 접근할 수 있다.

<!-- App.vue -->
<div id="app">
  counter : {{ $store.state.counter }} <br/>
  <button @click="addCounter">+</button>
  <button @click="subCounter">-</button>
</div>

<script>
export default {
  methods: {
    addCounter() {
      this.$store.state.counter++;
    },
    subCounter() {
      this.$store.state.counter--;
    }
  }
};
</script>

 

스크립트단에서 this.$store.state.counter로 state의 counter 속성값을 가져올 수 있다.

 

state 값을 가져오는 코드는 여러 컴포넌트에서 많이 사용될 가능성이 높기 때문에,

중복코드를 제거하고 좀 더 간결한 코드를 작성하기 위해 Vuex의 getters를 활용하는 것이 좋다.

 

Getters

 getters를 Vuex에 추가하여 단순한 조작을 컴포넌트의 computed 속성에서 사용해본다.

// store.js
export const store = new Vuex.Store({
  state: {
    counter: 0
  },
  
  getters: {
    getCounter: function (state) {
      return state.counter;
    }
  }
});

(매개변수인 state는 counter를 가져오기 위해 넣어주어야 한다)

 

그리고 App.vue에서 getters를 사용한다.

<!-- App.vue -->
<div id="app">
  counter : {{ parentCounter }} <br/>
  <button @click="addCounter">+</button>
  <button @click="subCounter">-</button>
</div>

<script>
export default {
  methods: {
    addCounter() {
      this.$store.state.counter++;
    },
    subCounter() {
      this.$store.state.counter--;
    }
  },
  computed: {
      parentCounter() {
        this.$store.getters.getCounter;
      }
  },
};
</script>

이전에는 template 단에서도 $store.state.counter 과 같이 가독성이 조금 떨어지는 코드를 직접 사용하였지만

computed에서 getters를 호출하여 사용함으로써 좀 더 간결한 코드를 작성할 수 있었다.

 

위에서는 단순하게 state 값을 가져오는 로직을 사용해서 별로 큰 차이를 못 느낄 수 있지만

getters에서 filter()나 reverse() 등의 추가적인 계산 로직이 들어간다면 더 큰 차이를 느낄 수 있을 것이다.

그리고 computed는 캐싱 효과가 있다는 점도 고려하면 전보다 많은 이점이 존재한다는 것을 이해할 수 있을 것이다.

 

mapGetters

Vuex에 내장된 helper 함수인 mapGetters를 활용하면 더 직관적이고 가독성 좋은 코드를 작성할 수 있다.

 

// App.vue
import { mapGetters } from 'vuex'

// ...
computed: ...mapGetters({
  // getCounter 는 Vuex 의 getters 에 선언된 속성 이름, parentCounter는 컴포넌트에서 사용할 속성 이름
  parentCounter : 'getCounter' 
}),

mapGetters 앞의 ...은 ES6의 Spread Operator(스프레드 연산자)를 사용한 것으로, 

다른 computed 속성과 함께 사용하기 위해 쓰는 문법이다.

(이해하기보다는 일단 문법 자체로 받아들이면 된다. mapGetters로 가져온 것을 computed에서 사용할 수 있도록 뿌려준다고 생각.

자세한 내용은 ES6의 Spread Operator를 찾아보면 어느정도 이해가 될 것이다)

 

 

Mutations

Mutations 이란 Vuex 의 데이터, 즉 state 값을 변경하는 로직들을 의미한다. Getters 와 차이점은

  1. 인자를 받아 Vuex 에 넘겨줄 수 있다.
  2. computed 가 아닌 methods 에 등록한다.

또한, 뒤에 나올 Actions 와의 차이점이다.

  • Mutations 는 동기적 로직을 정의한다.
  • Actions 는 비동기적 로직을 정의한다.

Mutations 의 성격상 안에 정의한 로직들이 순차적으로 일어나야 각 컴포넌트의 반영 여부를 제대로 추적할 수 있기 때문이다.

여태까지 우리는 counter 를 변경할 때

return this.$store.state.counter++;
return this.$store.state.counter;

와 같이 컴포넌트에서 직접 state 에 접근하여 변경하였지만, 이는 안티패턴으로써 Vue 의 Reactivity 체계와 상태관리 패턴에 맞지 않은 구현방식이다.

안티패턴인 이유는 여러 개의 컴포넌트에서 같은 state 값을 동시에 제어하게 되면, state 값이 어느 컴포넌트에서 호출해서 변경된건지 추적하기가 어렵기 때문이다.

하지만, 상태 변화를 명시적으로 수행함으로써 테스팅, 디버깅, Vue의 Reactive 성질 준수 의 혜택을 얻는다.

 

즉, Mutations을 거쳐감으로써 값이 어디서 무엇을 통해 어떻게 변하는지 추적할 수 있는 것이다.

Java의 Setter를 생각하면 된다. 값을 직접 바꾸는 것이 아닌 Setter를 통해서 값을 변경하는 것.

 

 

Vuex에 mutations 속성을 추가해본다.

 

// store.js
export const store = new Vuex.Store({
  // ...
  mutations: {
    addCounter: function (state, payload) {
      state.counter++;
    }
  }
});

counter 값을 1 올리는 로직을 mutations에 추가하였다.

 

// App.vue
methods: {
  addCounter() {
    // this.$store.state.counter++;
    this.$store.commit('addCounter');
  }
},

mutations에 등록된 속성은 컴포넌트의 methods 부분에서 등록해야 하는 것을 다시 한 번 체크하자.

그리고 getters와는 다르게 commit을 이용해 mutations 이벤트를 호출하고 있다.

추적 가능한 상태 변화를 위해 프레임워크가 이렇게 구조화 되어 있는 것이다.

 

각 컴포넌트에서 Vuex 의 state 를 조작하는데 필요한 특정 값들을 넘기고 싶을 때는 commit() 에 두 번째 인자를 추가할 수 있다.

this.$store.commit('addCounter', 10);
this.$store.commit('addCounter', {
  value: 10,
  arr: ["a", "b", "c"]
});

여러 값을 넘겨야 할 때는 객체에 담아 보낸다.

mutations: {
  // payload 가 { value : 10 } 일 경우
  addCounter: function (state, payload) {
    state.counter = payload.value;
  }
}

데이터의 매개변수명은 보통 payload라는 명칭을 많이 쓰지만 다른것을 사용해도 된다.

 

mapMutations

mapGetters와 마찬가지로, Vuex에 내장된 mapMutations를 활용하여 더 직관적이고 가독성 좋은 코드를 작성할 수 있다.

 

// App.vue
import { mapMutations } from 'vuex'

methods: {
  // Vuex 의 Mutations 메서드 명과 App.vue 메서드 명이 동일할 때 [] 사용
  ...mapMutations([
    'addCounter'
  ]),
  // Vuex 의 Mutations 메서드 명과 App.vue 메서드 명을 다르게 매칭할 때 {} 사용
  ...mapMutations({
    // addCounterOne 는 컴포넌트에서 사용하는 메서드명을, addCounter 는 Vuex의 Mutations를 의미
    addCounterOne: 'addCounter' 
  })
}

 

Actions란?

Mutations 에는 순차적인 로직들만 선언하고 Actions 에는 비 순차적 또는 비동기 처리 로직들을 선언한다. 그렇다면 왜 처리 로직의 성격에 따라 Mutations 과 Actions 로 나눠 등록해야 할까?

Mutations 에 대해 잠깐 짚어보면, Mutations 의 역할 자체가 State 관리에 주안점을 두고 있다. 상태관리 자체가 한 데이터에 대해 여러 개의 컴포넌트가 관여하는 것을 효율적으로 관리하기 위함인데 Mutations 에 비동기 처리 로직들이 포함되면 같은 값에 대해 여러 개의 컴포넌트에서 변경을 요청했을 때, 그 변경 순서 파악이 어렵기 때문이다.

 

따라서, setTimeout() 이나 서버와의 http 통신 처리 같이 결과를 받아올 타이밍이 예측되지 않은 로직(비동기)은 Actions 에 선언하고 동기 처리 로직은 Mutations에 선언한다.

 

example)

// store.js
export const store = new Vuex.Store({
  actions: {
    getServerData: function (context) {
      return axios.get("/product/detail");
    },
    delayFewMinutes: function (context) {
      return setTimeout(function () {
        commit('addCounter');
      }, 1000);
    }
  }
});

위처럼 HTTP get 요청이나 setTimeout 과 같은 비동기 처리 로직들은 actions 에 선언해준다.

 

 

 

 

Vuex에 Actions를 등록해본다.

// store.js
export const store = new Vuex.Store({
  // ...
  mutations: {
    addCounter: function (state, payload) {
      return state.counter++;
    }
  },
  actions: {
    addCounter: function (context) {
      // commit 의 대상인 addCounter 는 mutations 의 메서드를 의미한다.
      return context.commit('addCounter');
    }
  }
});

상태가 변화하는 걸 추적하기 위해 actions 는 결국 mutations 의 메서드를 호출(commit) 하는 구조가 된다.

 

 

// App.vue
methods: {
  // Actions 이용
  addCounter() {
    this.$store.dispatch('addCounter');
  }
},

App.vue에서는 dispatch()로 addCounter를 호출한다.

 

 

흐름은 다음과 같다.

 

dispatch로 mutations의 함수를 호출하고, 그 함수에 의해 값이 변화되는 구조이다.

 

Actions도 Mutations와 유사하게 인자를 넘길 수 있다.

export const store = new Vuex.Store({
  actions: {
    // payload 는 일반적으로 사용하는 인자 명
    asyncIncrement: function (context, payload) {
      return setTimeout(function () {
        context.commit('increment', payload.by);
      }, payload.duration);
    }
  }
})

 

 

mapActions

 

mapGetters, mapMutations 헬퍼 함수들과 마찬가지로 mapActions 도 동일한 방식으로 사용할 수 있다.

// App.vue
import { mapActions } from 'vuex';

export default {
  methods: {
    ...mapActions([
      'asyncIncrement',
      'asyncDecrement'
    ])
  },
}

 

 

모듈화

앞에 내용에서는 하나의 store.js에서 모든 state와 getters, mutations, actions를 다 관리하였다.

하지만 프로젝트의 규모가 커질수록 state의 속성들과 getters, mutations, actions에 등록되어 있는 속성들이 엄청 많아질 것이고 이것 역시 관리가 힘들어진다.

(mutations에 100개, 200개, 그 이상의 함수가 있다고 생각해보면 답이 나온다)

 

그래서 보통은 store 모듈을 컴포넌트 단위로 분리하여 작성하고, store.js 파일(중심)에서 모듈을 등록하여 Vuex를 사용한다.

 

예를 들면

TodoList.vue라는 컴포넌트에서 사용하는 store 모듈을 todoList.js로 명명하고 store.js 파일에 모듈로 등록하는 경우이다.

 

먼저 todoList.js 를 만들어보자.

// todoList.js

const state = {
	count : 0,
};

const getters = {
	//...
};

const mutations = {
	addCount(state) {
    	state.count++;
    }
};

const actions = {
	//...
};
  
export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
};

마지막에 export default 를 통해 어떤 것을 export 하여 다른 곳에서 쓸 수 있도록 할지 정할 수 있다.

위에서는 state, getters, mutations, actions를 모두 export 대상에 넣었지만 필요에 따라 넣고 뺄 수 있다.

 

namespaced: true는 state, getters, mutations, actions에 선언된 함수들을 구별할 수 있도록 앞에 namespace(이름)을 붙여주는 것이다.

 

예를 들어 todoList.js가 아닌 다른 모듈(anotherTodoList.js가 있다고 가정)에서 우연히 같은 이름과 형식의 함수를 정의했다면

컴포넌트에서 this.$store.commit('addCount') 을 실행했을 때 어떤 모듈에서 불러온 함수인지 구분할 수 없어서 문제가 생길 것이다.

// anotherTodoList.js 모듈
const mutations = {
	addCount(state) {
    	state.count++;
    }
};

따라서 앞에 이름을 붙여줌으로써 어떤 모듈 파일의 어떤 함수를 호출했는지 구분하게 만들어준다.

(파일명인 todoList가 모듈의 namespace가 된다)

 

export 설정을 다 했으면 이제 store.js에 모듈로 등록하면 된다.

// store.js

import Vue from 'vue'
import Vuex from 'vuex'
import todoList from './modules/todoList'

Vue.use(Vuex);

export const store = new Vuex.Store({
    modules: {
        todoList,
    }
});

 

App.vue에서 다음과 같이 addCount를 사용할 수 있다.

// App.vue
methods: {
  addCounter() {
    this.$store.commit('todoList/addCount');
  }
},

 

 

 

 

이상으로 Vuex를 활용한 상태관리에 대해 알아보았습니다.

 


다음의 글에서 많은 참고를 하였습니다

https://joshua1988.github.io/web-development/vuejs/vuex-start/

 

Vuex 시작하기 1 - Vuex와 State

Vue 중급으로 레벨업 하기. 상태 관리란 무엇인가? Vuex를 이용한 상태 관리. state 소개

joshua1988.github.io

 

'Vue.js' 카테고리의 다른 글

vee-validate  (0) 2023.01.19
03 인스턴스 & 컴포넌트 - 뷰 컴포넌트 통신  (0) 2022.11.11
03 인스턴스 & 컴포넌트 - 뷰 컴포넌트  (0) 2022.11.11
03 인스턴스 & 컴포넌트 - 뷰 인스턴스  (0) 2022.11.11
Data Binding  (0) 2022.11.03

+ Recent posts