구축한 Vue 애플리케이션의 규모가 커질수록 애플리케이션 상태, 데이터 관리도 점점 더 어려워 질 것이다. Vuex는 그러한 고민을 도와줄 뛰어난 상태 관리 솔루션이다. 여러 컴포넌트 또는 앱 전체에서 공유하고 싶은 데이터를 교체, 제공, 주입하는데 사용할 수 있다.
vuex는 무엇이고 왜 사용할까? vuex의 원리와 사용법은 무엇일까? 차근차근 살펴보자!
Vuex란 무엇일까?
Vuex is library for managing global state
Vuex는 글로벌 상태를 관리하기 위한 라이브러리다.
짚고 넘어가야 할 몇가지 개념들이 있다.
🤔 state란 뭘까?
state란 반응형 데이터이다. 1.변경할 수 있으며, 2.변경함으로써 화면상의 무언가를 촉발한다. 이런 state는 2가지 종류로 구분할 수 있다. global state, local state가 그것이다.
지역 상태란 하나의 컴포넌트 내부에서 관리되는 데이터, 상태이다. 하나의 컴포넌트에만 영향을 주고 프로퍼티를 통해 간접적으로 자식 컴포넌트에도 영항을 준다.
여러 컴포넌트 혹은 앱 전체에 걸쳐 영향을 주는 데이터, 상태를 말한다. 로그인(사용자 인증 상태), 장바구니 등이 예시가 될 수 있다. vuex를 배우지 않았던 지금까지는 provide, inject를 통해 전역 상태를 다뤄왔었다.
🤔 그래, 상태(state)라는 개념은 알겠다. 전역 상태를 다루기 위해 vuex를 사용한다는 것도 이해했다. 근데 굳이 Vuex여야 하나? 지금까지 써왔던 provide, inject로는 안되나?
전역 상태를 다루는 방식 중 vuex가 추천되는 이유에 대해 알아보자. 지금까지 사용해왔던 privde, inject 방식에는 잠재적인 단점이 있다.
일례로, 모든 데이터와 데이터를 변경하는 모든 메서드를 하나의 컴포넌트에 넣어야 한다. 이는 컴포넌트가 과도하게 커지는 것을 야기한다. 애플리케이션이 커짐에 따라 관리해야하는 전역 상태도 커지면서 유지 관리가 어려워질 수 있다. 특히 provide와 inject의 경우 반응성과 관련된 복잡한 문제를 일으킬 수도 있다.
다른 컴포넌트엔 필요하더라도 해당 컴포넌트 템플릿엔 필요 없는 엄청나게 많은 로직이 포함된 거대한 컴포넌트가 만들어질지도 모른다.
어디서 상태가 변경되는지 눈에 띄지 않거나 원하지 않는 방향으로 상태가 변경될 수도 있다.
실수로 상태 업데이트가 발생하거나 누락될 수 있으니 오류도 자주 발생할 것이다.
3줄 요약
앱 전체에서 다뤄야하는 전역변수들이 있다.
산발적으로 다루면 예상못하는 여러가지 오류들을 야기할 수 있다.
vuex로 깔끔하게, 한곳에서 다뤄보자!
Vuex로 카운터 상태를 관리해보자.
npm install --save vuex
<template>
<base-container title="Vuex">
<h3> 0 </h3>
<button>Add 1</button>
</base-container>
</template>
<script>
import BaseContainer from './components/BaseContainer.vue';
export default {
components: {
BaseContainer,
},
};
</script>
지금까지 흔히 봐오던 vue 컴포넌트다. 0이라는 숫자는 count를 의미하고, button을 클릭하면 해당 숫자가 1씩 증가하도록 구현하려고 한다. 우린 이미 이같은 과정을 익히 반복해왔기 때문에, data를 통해 count라는 변수를 연결하고, button으로 +1 이벤트를 발생시키는 방법으로 구현할 수 있다. 그러나 이번에는 vuex를 이용해, count라는 변수를 전역 상태로 다뤄보려고 한다!
import { createApp } from 'vue';
import {createStore} from 'vuex';
import App from './App.vue';
const store = createStore( {
state() {
return {
counter: 0
};
}
});
const app = createApp(App);
app.use(store);
app.mount('#app');
main.js에 애플리케이션 상태를 저장하는 Vuex 저장소를 생성해보자!
import { createStore } from 'vuex';
const store = createStore( {
state() {
return {
counter: 0
};
}
createStroe는 저장소를 구성한 객체를 취한다. 저장소는 전체 애플리케이션 당 하나만 가진다. (여러개의 상태가 있을 수 있다.)
state
app.use(store);
이제 이 컴포넌트에 있는 상태를 전체 앱의 모든 컴포넌트에서 사용할 수 있게 되었다! inject, provide같은 기능 없이, 자유롭게 해당 컴포넌트에 접근해 사용 가능하다.
🤔 그럼 해당 저장소의 상태에 접근은 어떻게 할 수 있을까?
<template>
<base-container title="Vuex">
<h3>{{ $store.state.counter }}</h3>
<button @click='addOne'>Add 1</button>
</base-container>
</template>
Vuex 저장소에서 관리하는 state를 가리키는 프로퍼티
위 코드에서는 버튼을 눌러도 +1 작업이 이루어지지 않는다. 아직 store과 mtehods, computed 등으로 이벤트를 연결하지 않았기 때문이다! 이벤트와 state를 연결하는 두가지 방법 모두 살펴보자.
<template>
<base-container title="Vuex">
<h3>{{ $store.state.counter }}</h3>
<button @click='addOne'>Add 1</button>
</base-container>
</template>
<script>
import BaseContainer from './components/BaseContainer.vue';
export default {
components: {
BaseContainer,
},
computed: {
counter() {
return this.$store.counter;
}
},
methods: {
addOne() {
this.$store.state.counter ++;
}
}
};
</script>
이제 우리는 전역적인 접근법에 대해 알아보았다. vuex덕분에 이제 우리는 vue 앱 어디서든 counter이라는 변수에 자유롭게 접근 가능하다!
여기저기 산발적으로 퍼져있는 전역 상태를 간단하게 다룰 sotre에 대해 알아보았다. 🤔 위의 App.vue 코드로 전역 상태를 완벽하게 다루고 있을까?
사실, 크게 아쉬운 부분이 있다.
this.$store.state.counter ++;
우리는 이 부분에서 store의 변수를 컴포넌트 내부에서 변경했다. 이렇게 코드를 작성하면 작동은 하나 이상적인 코드라 보기 힘들다. 이런식으로 전역 상태를 변경하면 애플리케이션 어디에서나 상태 변경이 가능하고, 이는 예측 불가능 하며 오류 가능성이 있다. 상태 변화 메커니즘에 대한 명확한 정의가 없기 때문이다.
앱 전반을 아우르는 데이터 저장소인 store가 있다. 우린 컴포넌트에서 이 store와 통신하나, 이는 직접적이어서는 안된다. (컴포넌트 곳곳에서 산발적으로 직접적인 통신을 하는 행위는 예측 불가능하며, 고로 오류를 야기할 가능성이 있다.)
methods: {
addOne() {
this.$store.state.counter += 11;
}
이처럼 누군가 counter를 증가시키는 메서드를 +1이 아니라 +11로 오타를 내더라도, counter에 직접 접근하는 코드가 앱 전반에 흩뿌려져 있기 때문에 해당 오류를 예방하거나 검수하는 일이 쉽지 않아진다. 뿐만 아니라, 만약 +1이 아니라 +2로 기능이 변경된다면? 우리는 해당 store에 접근하는 코드 모두 수정해야 한다.
대신 Vuex에는 내장된 개념인 변형(Mutations)을 제공하고 있다.
mitation는 명확하게 정의된 메서드로 상태를 업데이트하는 로직을 가지고 있다. 컴포넌트 내부에 있으며 직접 상태를 바꾸는 대신에 변형을 촉발(trigger)한다. 변형을 촉발한다는 공통된 하나의 개념으로, 모든 컴포넌트는 같은 방식으로 동작하는 것이 "보장"된다.(비몽사몽 졸면서 개발하던 개발자가 +1을 +11로 오타내는 것을 예방할 수 있는 것이다.)
const store = createStore( {
state() {
return {
counter: 0
};
},
mutations: {
increment(state) {
state.counter = state.counter +2;
}
}
});
이제 상태를 바꿔야하는 모든 곳에서 이 mutations를 사용할 수 있다. 필요한만큼 mutations를 추가할 수도 있다.
상태를 변화mutations시키는 것에 대해 정의했다. 변화는 어떻게 야기시킬 수 있을까?
<template>
<button @click="addOne">Add1</button>
</template>
<script>
export default {
methods: {
addOne() {
this.$store.commit('increment');
}
}
}
</script>
this.$store.commit('Mutations명');
특정 mutations는 파라미터를 요구할수도 있다. vuex는 변형이 커밋(commit)될 때 특정 인수를 가져오는 기능도 제공하고있다.
그럴땐 payload를 사용하면 된다.
다른 컴포넌트에서 같은 종류의 데이터가 필요할 때, 데이터의 형식을 바꾸거나 계산값을 다르게 바꾸어야 한다면, 다른 모든 장소에서 해당 코드를 수정해야 한다. 이같은 이유로 우리는 저장소의 데이터를 컴포넌트에서 직접적으로 통신하지 않았다. 데이터를 가져오는 방법을 간접적으로 구현하기 위해 게터(Getters)를 사용할 수 있다.
연산(computed) 프로퍼티와 같이 저장소에 직접 정의되어 우리가 원하는 컴포넌트 내부에서 사용할 수 있다.
const store = createStore({
store() {
return {
counter: 0
};
},
mutations: {...},
getters: {
finalCounter(state) {
return state.counter * 2;
}
이제 호출부에선 getters를 호출해서 상태를 불러올 수 있다.
export default {
computed: {
counter() {
return this.$store.getters.finalCounter;
}
},
}
Mutations, Getters외에 Vuex가 제공하는 핵심 개념이 하나 더 있다.
🧐state를 변형하는 mutations코드에서 비동기식 코드가 작성 가능할까?
mutations: {
increment(state) {
state.counter = 여기에 "2초후에 couter에 +2를 더하는 코드"를 작성할 수 있을까?
}
}
변형Mutations에 있어 비동기적으로 상태state를 변형해야 하는 경우가 있을 수 있다. (Http 응답 후 변형 등) 비동기식으로 실행하는 어떤 코드는 즉시 실행을 완료하는 것이 아니라 미래의 특정 시점에 완료하게 된다.
문제는, Mutations는 언제나 동기식이라는 점이 문제다. 변형에는 비동기적 코드(기다렸다가 실행하는 행위)가 허락되지 않는다. 만약 여러 변형이 일어나는 경우 모든 변형이 최신 상태를 받아야만 하기 때문이다. 만약 다른 변형이 커밋되고 실행이 완료되지 않는다면 예상치 못한 프로그램 오류를 야기할 수 있다.
처음 commit이라는 단어를 봤을때부터 DB와 유사하다는 느낌을 받은 것 같다. DB에서도 무언가 정보를 저장하고 변경하는 과정에서, 입력과 저장 사이의 시간이 소요되면 데드락과 같은 여러가지 문제가 야기된다. Mutations에 있어서도 동일할 것이다. 상태를 나중에 저장하겠다는 아이디어가, 전범위적으로 모두가 접근 가능한 전역 상태에게는 치명적이고 자주 유발될 수 있는 오류를 의미하는 개념일 것이다.
이처럼 상태state에 있어 비동기적 코드를 다루기 위해 Vuex에서 제공하는 더 나은 개념이 있다. 바로 Actions이다!
actions의 움직임은 다음과 같다.
컴포넌트 >--> Actions >--> Mutations
컴포넌트는 액션을 트리거하고 이는 변형을 커밋한다. 액션은 비동기식 코드를 사용할 수 있기 때문에 component와 mutations 사이에 actions을 넣는 것은 일반적으로 좋은 방식으로 여겨진다.
물론 컴포넌트가 바로 mutation을 트리거해도 된다. 그러나 actions을 그 사이에 두는 것이 권장된다. 동기식 코드만을 사용한다면 문제될 것이 없고 컴포넌트 내부에서 변형을 직접 커밋하는 것도 종종 문제가 되지 않는다. 그럼에도 액션을 사이에 넣는 것은 좋은 관행으로 여겨지는데 Mutations에 실수로 비동기식 코드를 넣는 것을 방지해주기 때문이다.
actions: {
increment(context) {
setTimeout(function() {
context.commit('increment');
}, 2000);
},
increase(context, payload) {
context.commit('increase', payload);
}
}
methods: {
addOne() {
this.$sotre.dispatch({
type: 'increase',
value: 10
});
},
}
commit을 디스패치(dispatch)로 변경한다.
컨텍스트(context)에는 흥미로운 요소들이 많다.
console.log(context);
를 통해 context가 담고있는 흥미로운 요소들을 살펴볼 수 있다.
store, state가 많아지고 거대해지면, Vuex가 제공하는 기능 중 모듈 설정 기능이 유용할 수 있다. 애플리케이션에 하나의 저장소가 있고, 그 저장소가 다수의 모듈로 이루어져 코드를 관리하기 쉬워지는 것이다.
모듈을 따로 설정하지 않으면 자동으로 하나의 루트 모듈과 상태를 가지게 된다. 모듈은 원하는 만큼 추가 가능하다.
const counterModule = {
state() {
return {
counter:0
}
},
mutations: {},
actions: {},
getters: {}
};
이처럼 이미 저장소에 만든 특정 상태(state)를 따로 밖으로 위임하여 모듈로 객체를 생성할 수 있다.
const store = createStore({
modules: {
numbers: counterModule
},
}
모듈 내의 상태state는 모듈내의 지역(local)상태로 치부된다. 변형, 액션, 게터는 전역(global)이라 메인 저장소에서 전처럼 엑세스 가능하나 상태는 모듈의 지역 상태이므로 모듈 내 상태는 해당 모듈에만 적용된다.
const counterModule = {
namespaces:true,
state() {
....
namespaces:true
: 모듈 전체가 저장소로부터 분리되어야 한다는 것을 vuex에게 알린다.
main.js가 너무 크다. sotre.js로 저장소 코드를 옮기자!