본문 바로가기
Study

[Vue.js] Vuex (feat. Flux pattern)

by 안자두 2023. 5. 28.

Vuex란?

Vue.js를 위한 상태 관리 패턴이면서 라이브러리이다. 상태가 예측 가능한 방식으로만 변경될 수 있도록 하는 규칙을 사용하여 응용 프로그램의 모든 구성 요소에 대한 중앙 집중식 저장소 역할을 한다.

각 컴포넌트들이 관리하고 있는 상태를 props로 전달하는 것에는 한계가 있다. 또한 상태를 변경하기 위해 event를 emit 하는 것은 더욱 복잡해진다.
이때, 중앙 집중화된 상태 정보를, 사용하는 컴포넌트에서만 갖다 쓰는 방식이라면, props로 계속 넘겨주지 않아도 되고, 유지보수 측면에서도 훨씬 유리해진다.

Vue는 MVVM 모델이기 때문에 상태가 바뀌면 ViewModel 객체가 바라보고 있다가 감지하여 UI를 자동으로 변경하는데, 이렇게 중요한 상태가 어느 컴포넌트나 메서드에 의해서, 언제 변경되는지를 전혀 알 수 없게 된다.
그래서 상태 정보가 변경되는 상황과 시간을 추적할 수 있으면서 컴포넌트에서 상태 정보를 안전하게 접근할 수 있는 Vuex 라이브러리를 사용한다.

 

작은 규모의 Vue 앱을 생성할 때는 굳이 Vuex를 사용할 필요가 없다. 오히려 오버 엔지니어링이 될 수 있다. 하지만 중간 규모 이상일 경우에는 Vuex를 사용해 상태를 보다 효과적으로 관리할 필요가 있다.

 


 

Flux Pattern

MVC 패턴의 경우, Model과 View가 많아지면 기능 추가 및 변경에 따라 생기는 문제점을 예측하기 힘들어진다. 또한, 앱이 복잡해지면서 업데이트 loop가 생긴다.

Flux는 이러한 MVC 패턴의 복잡한 데이터 흐름 문제를 해결하기 위한 개발 패턴이다.

Flux Pattern Diagram

위의 다이어그램에서 볼 수 있듯 데이터는 단방향으로만 흐르고, 새로운 데이터를 넣으면 처음부터 흐름이 다시 시작된다.

패턴 각 단계의 아래부분에 각각에 해당하는 vuex 요소를 작성해 보았다. 아래의 vuex 흐름도와 연관 지어서 볼 수 있다.

 


 

Vuex Architecture

출처: https://vuex.vuejs.org/#what-is-a-state-management-pattern

vuex는 flux 패턴을 따르기 때문에 데이터가 단방향으로만 흐른다.

먼저, 컴포넌트가 event같은 actions을 일으키면, (1) actions에서는 API를 호출한 뒤, 그 결과를 이용해 (2) mutations를 일으킨다. mutations에서는 actions의 결과를 받아 (3) state를 변경한다. 변경된 state는 다시 컴포넌트에 바인딩되어 (4) UI를 갱신한다.

이 과정은 추적이 가능하므로 DevTools와 같은 도구를 이용하면 상태 변경의 내역을 모두 확인할 수 있다.

vuex를 사용하게 되면,

  • MVC 패턴에서 발생할 수 있는 구조적 오류를 해결
  • 컴포넌트 간 데이터 전달 명시
  • 여러 개의 컴포넌트에서 같은 데이터를 업데이트할 때 동기화 문제를 해결

할 수 있게 된다.

 


 

Vuex 시작하기

우선 기본 vuex를 설치해보자.

CDN으로 추가를 할 수도 있고, 

<script src="https://unpkg.com/vuex@4.0.0/dist/vuex.global.js"></script>

npm으로 설치할 수도 있다.

npm install vuex@next --save

 

모든 Vuex 앱의 중심에는 store가 있다. "store"는 기본적으로 애플리케이션 상태를 보관하는 컨테이너이다. Vuex와 일반 글로벌 객체와는 두 가지 차이점이 있다.

1. Vuex는 반응형으로, store의 state가 변경되면 반응적이고 효율적으로 업데이트된다.
2. state를 직접 변경할 수 없다. store의 state를 변경하는 유일한 방법은 명시적으로 mutations을 커밋하는 것이다. 이를 통해 모든 상태 변경이 추적 가능한 기록을 남기고 애플리케이션을 더 잘 이해하는 데 도움이 되는 도구를 사용할 수 있다.

 

이제 store를 생성해 보자.

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {}
})

생성된 store는 Vue 인스턴스에 종속시켜 사용할 수 있다.

import Vue from 'vue';
import App from './App.vue';
import { store } from './store';

Vue.config.productionTip = false;

new Vue({
	store,
	render: h => h(App),
}).$mount('#app');

 

store에는 state, getters, mutations, actions가 있다. 이제 각각에 대해 설명해 보겠다.

 


 

State

state는 여러 컴포넌트 간 공유할 데이터 속성을 의미한다. Vue 인스턴스의 data 속성과 동일한 역할을 한다.

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    num: 1
  }
});

state는 다음과 같이 작성할 수 있으며, num은 this.$store.state.num으로 접근할 수 있다.

만약 state가 변경되는 값이라면, Vue 인스턴스의 computed 속성에서 이 값을 반환해 사용할 수 있다. computed는 값의 변화를 감지하기 때문에 DOM 업데이트를 돕는다.

export default {
    computed: {
        num () {
        	return this.$store.state.num
        }
    }
}

 


 

Getters

 

state 값을 접근하는 속성이며, 미리 연산된 값이다. 각 컴포넌트에서 Vuex의 데이터를 접근할 때, 중복된 코드를 반복 호출하게 될 수 있는데, 이때 Vuex 자체에서 이 연산을 수행한 후, 각 컴포넌트에서 로직을 호출해서 사용하기만 하면, 코드 가독성도 올라가고 성능에도 이점이 생긴다.

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    num: 1
  },
  getters: {
  	doubleNum(state) {
		return state.num * 2;
    }
  }
});

첫 번째 인자에는 state가 들어간다. this.state가 아닌 이 state로 다른 속성에 접근할 수 있다.

사용 방법은 state와 동일하다.

export default {
    computed: {
        doubleNum () {
        	return this.$store.getters.doubleNum
        }
    }
}

 


 

Mutations

state 값을 변경할 수 있는 유일한 방법이자 메서드이다. getters와의 차이점은 인자를 받아 Vuex에 넘겨줄 수 있고, computed가 아닌 methods에 등록한다는 점이다.

state를 직접 변경하는 것은 어느 컴포넌트에서 언제 호출되었는지 추적하기 어렵다. 하지만 mutations로 상태를 변화하면 이 문제를 해결할 수 있다. 쉽게 setters라고 생각할 수 있다.

mutations의 역할 자체가 state 관리에 초점을 맞추고 있다. 상태 관리 자체가 한 데이터에 대해 여러 개의 컴포넌트가 관여하는 것을 효율적으로 관리하기 위함인데, mutations에 비동기 처리 로직도 포함되어 있으면 같은 값에 대해 여러 개의 컴포넌트에서 변경을 요청했을 때, 그 변경 순서 파악이 어렵기 때문에 뒤에 나오는 actions에 비동기 로직을 따로 처리하는 것이다.

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    num: 0
  },
  mutations: {
  	addSomeNum(state, payload) {
    	state.num = state.num + payload;
    }
  }
});

val이라는 인자를 넘겨받아 state의 num을 변경할 수 있다.

export default {
    methods: {
        addSomeNum () {
        	return this.$store.commit('addSomeNum', 4);
        }
    }
}

두 번째 params로는 인자를 넘길 수 있는데, 여러 값을 보내려면 객체 형태로 보내면 된다.

export default {
    methods: {
        addSomeNum () {
        	return this.$store.commit('addSomeNum', {id: 4, name: 'ktmihs'});
        }
    }
}

주의할 점은 this.$store.mutations로 접근하는 것이 아닌, commit으로 이벤트를 호출해야 한다는 점이다.

 


 

Actions

동기 로직만을 처리하는 mutations와는 다르게 actions는 비동기 처리 로직을 선언하는 메서드이다.

위에서 설명했듯이 state에 대한 변경 순서를 파악하기 위해 mutations와 분리되었다. 등록 및 사용 방법은 mutations와 유사하다.

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

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    num: 0
  },
  mutations: {
    setData(state, payload) {
    	state.num = payload;
    }
  },
  actions: {
    getAxiosData(context) {
            return axios.get('url').
                    then((res) => {
                            const { data } = res;
                            commit('setData', data);
                    }).
                    catch((err) => console.log(err));
    	}
    }
});

상태가 변화하는 것을 추적하기 위해 actions는 commit으로 mutations를 호출한다.

export default {
    methods: {
        getAxiosData () {
        	return this.$store.dispatch('getAxiosData');
        }
    }
}

actions의 경우에는 dispatch를 사용하여 호출하는 점이 조금 다르다.
인자를 넘기는 방법은 mutations와 동일하게 두 번째 params로 넘겨줄 수 있다.

 


 

Helper 함수

매번 this.$store.state.num 이런 식으로 갖다 쓰는 것은 불편하기도 하고 비효율적이기 때문에 Vuex에서는 이를 간단하게 갖다 쓸 수 있는 Helper 함수를 제공한다.

state와 getters는 computed 속성에서
mutations와 actions는 methods 속성에서 쓸 수 있다.

각각은 접두사 map을 붙여서 사용하면 된다.

export default {
  data() {
    return {
      num: this.$store.state.num,
      name: this.$store.state.name,
    }
  },
  computed: {
    getDoubleNum() {
      return this.$store.getters.getDoubleNum
    },
    getAge() {
      return this.$store.getters.getAge
    },
  },
  methods: {
    getOneContact() {
      return this.$store.commit('getOneContact');
    },
    getAxiosContacts() {
      return this.$store.dispatch('getAxiosContacts');
    },
  }
}

만약 helper 함수를 쓰지 않는다면 위의 코드처럼 작성해야 하지만, helper 함수를 사용한다면 아래와 같이 사용할 수 있다.

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

export default {
  computed: {
    ...mapState(['num', 'name']),
    ...mapGetters(['getDoubleNum', 'getAge'])
  },
  methods: {
    ...mapMutations(['getOneContact']),
    ...mapActions(['getAxiosContacts'])
  },
}

mapState()처럼 병렬로 작성하여 하나의 helper 함수에서 여러 개의 값을 가져올 수 있다. 

 


 

REF

https://bestalign.github.io/translation/cartoon-guide-to-flux/

https://steemit.com/kr/@stepanowon/vuex#store_modules

https://vuex.vuejs.org/#what-is-a-state-management-pattern

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

 

 

 

728x90