Vue : Vuex
1. Vuex란?
- Store라고도 불림.
- 무수히 많은 컴포넌트의 데이터를 관리하기 위한 상태 관리 패턴이자 라이브러리
- 서비스가 복잡해질수록 컴포넌트 간에 데이터 전달이 어려워지기 때문에 Vuex를 사용하는 것
- React의 Flux 패턴에서 기인함
Flux 패턴 ?
기존 MVC 패턴은 Model과 View 간 양방향 통신이 가능합니다. 하지만 서비스가 커질 수록 이런 양방향 통신이 데이터의 흐름을 예측할 수 없게 만듭니다.
때문에 이런 단점을 극복하고자 데이터의 흐름이 여러 갈래로 나뉘지 않고 단방향으로만 처리하는 Flux 패턴이 나오게됩니다.
2. Flow
Vuex의 Flow는 아래와 같이 이루어집니다.
- 컴포넌트 -> 비동기 로직 -> 동기 로직 -> 상태 변경
3. 설치 및 등록
- Vuex를 사용할 프로젝트로 이동
- 터미널에서 npm i vuex@3.6.2 –save (2022.02.07 부터 이 명령어로 사용)
- store 폴더 생성
store 폴더 ?
Vuex 폴더는 보통 store라는 이름으로 만듭니다. 해당 폴더는 관행적으로 프로젝트의 src 바로 아래에 store 폴더를 만듭니다.
- store 폴더에 store.js 생성
- 아래의 세팅을 참고
3-1. 세팅
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
});
Vue.use()는 Vue의 Plugin으로, Vue를 사용하는 모든 영역에 특정 기능을 추가할 때 사용합니다. 해당 세팅을 해주어야 다른 컴포넌트에서 this.$store로 Vuex를 사용할 수 있습니다.
main.js
import Vue from 'vue'
import App from './App.vue'
import {store} from "@/store/store";
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
main.js에 store(Vuex)를 등록해주어야 합니다.
4. 기능
4-1. state
컴포넌트 간에 공유하는 데이터를 말합니다. (Vue의 data()와 비슷) 아래와 같이 store.js에서 데이터를 등록하여 사용할 수 있습니다.
store.js (state 값 등록)
const storage = {
created() {
const arr = [];
if (localStorage.length > 0) {
for (let i = 0; i < localStorage.length; i++) {
const item = localStorage.getItem(localStorage.key(i));
const parsedItem = JSON.parse(item);
arr.push(parsedItem);
}
}
return arr;
},
}
export const store = new Vuex.Store({
state: {
todoItems : storage.created(),
}
});
TodoList.vue (state 데이터 사용)
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, index) in this.$store.state.todoItems" :key="todoItem.item" class="shadow">
<i class="checkBtn fa-solid fa-check" :class="{checkBtnCompleted: todoItem.completed}" @click="toggleComplete(todoItem, index)"></i>
<span :class="{textCompleted: todoItem.completed}"></span>
<span class="removeBtn" @click="removeTodo(todoItem.item, index)">
<i class="fa-solid fa-trash-can"></i>
</span>
</li>
</transition-group>
</div>
</template>
4-2. mutation
state 값을 변경하는 이벤트 로직이나 메서드를 말합니다. (Vue의 methods와 비슷)
왜 state를 직접 변경하지 않고 mutations로 변경할까?
여러 개의 컴포넌트에서 공유하는 데이터인 state 값을 그냥 변경해버리면, 어느 컴포넌트에서 해당 state를 변경했는지 추적하기가 어렵습니다.
때문에 Devtools로 반응성을 확인하고 테스트 할 수 있는 mutations를 이용하여 값을 변경하는 것이 좋습니다.
store.js (mutation 메서드 등록)
store의 mutations에 메서드를 등록해주면 됩니다. 이 때, mutations에 등록한 메서드의 첫 번째 인자는 state를, 두 번째 인자는 데이터 값을 받을 수 있습니다. 때문에 state값을 조작할 수 있는 것입니다.
export const store = new Vuex.Store({
state: {
todoItems : storage.created(),
},
mutations: {
addOneItem(state, todoItem) {
const obj = {completed: false, item: todoItem};
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
}
});
TodoInput.vue (mutations 메서드 사용)
store의 commit() 메서드로 mutation을 실행시킬 수 있습니다. commit 메서드의 첫 번째 인자는 메서드 이름을, 두 번째는 데이터를 넣을 수 있습니다. (만약 여러 데이터를 넣어야 한다면 객체로 생성하여 넣어주면 됩니다.)
methods: {
addTodo() {
if(!this.validateItem()) {
this.showModal = true;
return;
}
this.$store.commit('addOneItem', this.newTodoItem);
this.clearInput();
},
...
}
4-3. action
비동기 처리 로직을 선언하는 메서드를 말합니다. (Vue의 async methods와 비슷)
왜 비동기 처리 로직은 actions에 선언해야 할까?
비동기 로직은 시간 차가 있기 때문에 언제 어느 컴포넌트에서 값이 조작되는지 확인하기가 어렵습니다. 동기, 비동기 로직을 나누지 않으면 데이터를 확인하는데 큰 어려움이 있기 때문에 actions에 비동기 로직을 모아 놓는 것입니다.
store.js (action 메서드 등록)
store의 actions에 메서드를 등록해주면 됩니다. 이 때, actions에 등록한 메서드의 첫 번째 인자는 context를 받을 수 있습니다. (context는 store에 있는 메서드와 속성에 접근할 수 있습니다.)
export const store = new Vuex.Store({
state: {
product : '',
},
mutations: {
setData(state, fetchedData) {
state.product = fetchedData;
}
},
actions: {
fetchProductData(context) {
return axios.get('https://domain.com/products/1')
.then(response => context.commit('setData', response));
}
}
});
App.vue (actions 메서드 사용)
store의 dispatch() 메서드로 actions을 실행시킬 수 있습니다. dispatch 메서드의 첫 번째 인자는 메서드 이름을 넣어 실행 시킬 수 있습니다.
methods: {
getProduct() {
this.$store.dispatch('fetchProductData');
}
}
4-4. getters
연산된 state 값을 접근하는 속성을 말합니다. (Vue의 computed와 비슷) state를 가져올 때 getters로 다양한 연산이나, 규칙들을 적용하여 가져올 수 있습니다.
store.js (getters 메서드 등록)
store의 getters에 메서드를 등록해주면 됩니다. 이 때, getters에 등록한 메서드의 첫 번째 인자는 state를 받을 수 있습니다. 때문에 state값을 가져올 때 다양한 규칙이나 연산을 적용하여 가져올 수 있는 것입니다.
export const store = new Vuex.Store({
state: {
price : 100,
},
mutations: {
setData(state, fetchedData) {
state.product = fetchedData;
}
},
actions: {
fetchProductData(context) {
return axios.get('https://domain.com/products/1')
.then(response => context.commit('setData', response));
}
},
getters: {
originalPrice(state) {
return state.price;
},
doublePrice(state) {
return state.price * 2;
},
triplePrice(state) {
return state.price * 3;
},
}
});
App.vue (getters 메서드 사용)
store의 ‘getters.메서드명’ 으로 getters을 실행시킬 수 있습니다.
computed: {
originalPrice(state) {
return this.$store.getters.originalPrice;
},
doublePrice(state) {
return this.$store.getters.doublePrice;
},
triplePrice(state) {
return this.$store.getters.triplePrice;
},
}
5. Helper
Vuex의 Helper 함수는 아래와 같은 store의 4가지 속성들을 간편하게 코딩할 수 있게 도와주는 함수입니다.
- state -> mapState
- getters -> mapGetters
- mutations -> mapMutations
- actions -> mapActions
5-1. 사용 방법
Vuex의 Helper 함수를 사용할 때 공통적으로 파라미터에 배열과 객체를 넣을 수 있는데 두 경우의 차이는 아래와 같습니다. (예시는 mapState로 하겠습니다.)
// 배열 (배열에 넣은 이름과 동일하게 메서드 생성)
computed() {
...mapState(['num']) // mapState에 배열을 넣고 Spread 연산자를 이용한 결과는 아래와 같습니다.
num() { return this.$store.state.num; }
}
// 객체 (객체의 속성명으로 메서드 생성(이름 설정 가능))
computed() {
...mapState({number: 'num'}) // mapState에 객체를 넣고 Spread 연산자를 이용한 결과는 아래와 같습니다.
number() { return this.$store.state.num; }
}
mapState
Vuex에 선언한 state 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼입니다.
// App.vue
import { mapState } from 'vuex';
computed() {
...mapState(['num']) // mapState를 Spread 연산자를 이용한 결과는 아래와 같습니다.
num() { return this.$store.state.num; }
}
// store.js
state: {
num: 10
}
mapGetters
Vuex에 선언한 getters 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼입니다.
mapGetters를 사용하면 store의 state를 이용할 수 있는 getters 메서드를 사용하고자하는 vue파일에서 더 쉽게 사용할 수 있습니다.
// App.vue
import { mapGetters } from 'vuex';
computed() {
...mapGetters(['reverseMessage']) // reverseMessage getter를 현재 vue 파일에서 사용할 수 있게됨
}
// store.js
getters: {
reverseMessage(state) {
return state.msg.split('').reverse().join('');
}
}
mapMutations
Vuex에 선언한 mutations 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼입니다.
mapMutations를 사용하면 store의 state를 이용할 수 있는 mutations메서드를 사용하고자하는 vue파일에서 더 쉽게 사용할 수 있습니다.
// App.vue
import { mapMutations } from 'vuex';
computed() {
...mapMutations(['clickBtn']) // clickBtn mutation을 현재 vue 파일에서 사용할 수 있게됨
}
// store.js
mutations: {
clickBtn(state) {
alert(state.msg);
}
}
mapActions
Vuex에 선언한 actions 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 헬퍼입니다.
mapActions를 사용하면 store의 context를 이용할 수 있는 actions 메서드를 사용하고자하는 vue파일에서 더 쉽게 사용할 수 있습니다.
// App.vue
import { mapActions } from 'vuex';
computed() {
...mapActions(['delayClickBtn']) // delayClickBtn action을 현재 vue 파일에서 사용할 수 있게됨
}
// store.js
actions: {
delayClickBtn(context) {
setTimeout(() => context.commit('clickBtn'), 2000);
}
}
6. 파일 분리 하는 방법(모듈화)
서비스가 커질수록 하나의 store 파일 안에서 많은 코드를 관리하게 되면 복잡도가 증가하여 유지보수에 어려움을 겪게 됩니다. 때문에 아래와 같이 Vuex의 modules를 이용해 파일을 분리하는 모듈화가 필요합니다.
export const store = new Vuex.Store({
modules: {
todoApp,
todoApp2,
}
});
6-1. 모듈화 시 주의할 점
Vuex의 modules를 이용 시 각 모듈들을 namespace(modules에서 설정해준 속성명)으로 구분할 수 있습니다.하나의 모듈만 사용할 때는 굳이 namespace 구분을 해줄 필요없습니다.
그러나 getters, mutations, actions는 global namespace로 등록되는 반면, state는 global namespace로 등록되지 않기 때문에 state 사용 시 아래와 같이 모듈명을 명시해주어야 합니다. (해당 내용은 아래의 namesapce 부분에서 더 자세히 다룰 예정입니다.)
getters 접근
<template>
...
<li v-for="(todoItem, index) in this.storedTodoItems" :key="todoItem.item" class="shadow">
...
</template>
computed: {
// getters는 global namespace로 등록되기 때문에 template에서 바로 접근 가능
...mapGetters(['storedTodoItems']);
}
state 접근
<template>
...
<li v-for="(todoItem, index) in this.todoItems" :key="todoItem.item" class="shadow">
...
</template>
computed: {
// state는 global namespace로 등록되지 않아서 아래의 방식으로 template에서 바로 접근 불가능
// ...mapState(['todoItems']) ;
// state 접근 시 아래의 방법처럼 모듈 명을 명시해주어야 함
...mapState({todoItems: state => state.todoApp.todoItems,})
}
6-2. 파일 분리(모듈화)
기존의 state, getters, mutations, actions를 따로 js파일로 분리하여 불러오는 방식으로 모듈화를 진행할 수 있습니다.
// TodoApp
const todoApp = {
state: {
...
},
getters: {
...
},
mutations: {
...
},
actions: {
...
}
}
export default todoApp;
// TodoApp2
const state = {
...
}
const getters = {
...
}
const mutations = {
...
}
const actions = {
...
}
export default {
state,
getters,
mutations,
actions,
};
6-2. namespace로 모듈 구분
모듈을 나눈다고 하더라도 global namespace로 모듈을 사용하게 되면 이름의 중복이 많아질 수 있기 때문에 namespace를 사용해 모듈을 구분할 수 있습니다.
store에 namespaced를 true로 설정하면 todoApp/storedTodoItems
처럼 해당 객체 이름으로 모듈을 구분하여 사용할 수 있습니다.
// TodoApp
const todoApp = {
namespaced: true,
state: {
...
},
getters: {
...
},
mutations: {
...
},
actions: {
...
}
}
export default todoApp;
댓글남기기