Vue3에서 Vuex 사용해보기
Intro
안녕하세요. 김도겸입니다.
Vue의 상태관리 라이브러리인 Vuex의 개념과 구조 및 사용법에 대해 스터디한 것을 정리해보았습니다.
1. Vuex란???
Vue에서 data()에 해당하는 것, 즉, 컴포넌트 간 공유할 수 있는 데이터들을 상태(state)라고 부릅니다.
이 데이터들을 컴포넌트에 공유하기 위해서 props와 emit을 활용하지만 점점 규모가 커질수록 작은 단위로 쪼개진 여러개의 컴포넌트로 화면을 구성하고, 페이지 수가 늘어나며 데이터를 주고 받는 컴포넌트 간의 관계는 점점 복잡해질 수 밖에 없습니다.
이와 같은 문제점을 해결하기 위해 데이터 전달을 유기적으로 관리할 목적으로 Vue에서 지원하는 것이 여러 컴포넌트 간의 데이터 전달과 이벤트 통신을 한 곳에서 관리하는 패턴이자 라이브러리인 Vuex입니다.
2. Vuex의 구조
-
Vuex의 속성
Vuex는 state, mutaitions, action, getters 4가지의 형태로 관리가 되며, 단방향 데이터 흐름으로 처리하고, store라는 이름을 가진 파일로 관리하므로 통상 store라고 불립니다.-
state
state는 Vue 컴포넌트에서 data로 볼 수 있고, 원본 소스의 역할을 하며, View와 직접적으로 연결되어있는 Model입니다.
이 state는 직접적인 변경이 불가능하고 mutation을 통해서만 변경이 가능하다. mutation을 통해 state가 변경이 일어나면 반응적으로 view가 업데이트됩니다. -
mutaitions
mutation은 state를 변경하는 유일한 방법이고 이벤트와 유사합니다.
mutation은 함수로 구현되며 첫 번째 인자는 state를 받을 수 있으며, 두 번째 인자는 payload를 받을 수 있습니다.
여기서 payload는 여러 필드를 포함할 수 있는 객체 형태도 가능합니다. 일반적으로 직접 호출은 할 수 없으며, commit을 통해서만 호출할 수 있습니다.실무에서는 api를 통해 전달 받은 데이터를 가공하여 state를 설정하는 데 주로 사용됩니다.
-
actions
action은 mutation과 비슷하지만 비동기 작업이 가능하다는 차이점이 있습니다.
또한 mutation에 대한 commit 호출이 가능하여 action에서도 mutation을 통해 state를 변경할 수 있습니다.
action에서는 첫 번째 인자를 context 인자로 받을 수 있으며 이 context에는 state, commit, dispatch, rootstate와 같은 속성들을 포함하고, 두번째 인자는 mutation과 동일하게 payload로 받을 수 있습니다.
action은 dispach를 통해 호출이 가능하고, action에서는 서로 다른 action을 호출 할 수 있습니다.실무에서 actions는 Axios를 통한 API 호출과 그 결과에 대해서 반환을 하거나 mutation으로 commit하여 상태를 변경하는 용도로 주로 사용됩니다.
-
getters
getters는 Vue 컴포넌트에서 Computed의 역할을 해준다고 볼 수 있습니다.
특정 state에 대해 어떠한 연산을 하고, 그 결과를 View에 바인딩할 수 있으며, state의 변경 여부에 따라 getter는 재계산이 되고 View 역시 업데이트가 이루어집니다.
이때 state는 원본데이터로서 변경이 일어나지 않습니다.실무에서 state의 반복적인 연산 처리가 필요한 내용에 대해 getters를 사용합니다.
-
-
Vuex flow
Vuex를 사용하면 위 다이어그램과 같은 flow로 구성됩니다.- vue component에서 action 메서드 실행(dispatch)
- action을 통해 비동기 로직 처리 후에 Mutation 메서드 실행(commit)
- Mutation에서 데이터를 가공하여 state를 변경함
- 변경된 state를 vue component에 렌더링해줌.
3. 설치 및 사용법
-
설치 방법
Vue 프로젝트에 Vuex는 다음과 같이 설치하면 됩니다. (Vue3 기준) -
main.js에 store 세팅
import { createApp } from "vue"; import App from "./App.vue"; import store from "./store"; createApp(App) .use(store) .mount("#app");
-
store 폴더 생성 후 index.js 파일 생성
import { createStore } from "vuex"; export default createStore({ state: {}, getters: {}, mutations: {}, actions: {}, });
-
4가지 속성 기본적인 사용법 news api를 가져오는 프로젝트라고 가정했을 때 예제 코드입니다.
-
state
사용할 data를 다음과 같이 정의합니다.
state:{ news: [] },
이를 component에서 사용할 때에는 다음과 같이 사용합니다.
setup(){ const newsState = computed(() => store.state.news); }
컴포넌트 에서 사용하려면 computed로 묶어야만 state의 data 값이 바뀌는 것을 감지하고 반응하여 렌더링해줍니다.
-
getters
사용할 getter를 다음과 같이 정의합니다.
getters: getFirstNewsTitle(state){ return state.newsState[0].title; },
해당 코드는 state의 data인 newsState list의 가장 첫 아이템의 title을 리턴해줍니다.
최근순으로 따졌을 때 가장 최근 뉴스의 제목이 필요할때 사용하면 좋을 것 같습니다.setup(){ const firstNewsTitle = computed(() => store.getters.getFirstNewsTitle); console.log(firstNewsTitle.value); //getter가 여러 개 필요할 때는 const newsGetters = computed(() => store.getters); console.log(newsGetters.value.GETTER_NAME); }
컴포넌트에서 사용할 경우엔, state와 마찬가지로 computed로 묶어 사용해야 합니다.
-
mutations
mutation을 다음과 같이 정의하여 사용합니다.
mutations:{ setNews(state, news) { //동기적 로직 정의 가능 state.news = news } },
다음과 같이 간단한 setter처럼 구현합니다.
(setter처럼 구현하지 않고 다른 동기적 로직도 정의 가능합니다.)setup() { function resetData() { store.commit("setNews", []); } }
주로 실무에선 actions에서 호출하지만 컴포넌트에서 호출할 경우엔 위와 같이 호출하면 됩니다. (굳이 사용한다면 다음과 같이 간단히 리스트를 초기화해주는 정도로 사용 가능할 것 같습니다.)
-
actions
actions는 다음과 같이 정의하여 사용합니다.
import { fetchNewsList } from "../api/index" ... actions:{ getNewsData(context) { //fetchNewsList라는 GET API 호출 메서드가 있다고 가정함. fetchNewsList() .then(response => { // mutation 호출 context.commit('setNews', response.data); }) .catch(error => { console.log(error) }) } },
위와 같이 api 호출 등의 비동기 로직을 actions에서 정의하여 사용합니다.
주로 비동기 로직 처리 후에 mutation을 호출의 state의 데이터를 변경해주는 코드까지 작성합니다.setup() { function newsData() { store.dispatch("getNewsData"); } }
컴포넌트에서 호출할 경우에 위와 같이 사용하면 됩니다. (function으로 묶지않고 setup 생명주기에 바로 실행시켜도 상관없습니다.)
-
4. store를 분리하기
실무에선 위 예제처럼 하나의 store 파일로는 관리할 수 없기 때문에 상황에 따라 적절히 분리하여 사용해야 합니다.
분리하는 방법이 2가지가 있어 나눠서 설명드리도록 하겠습니다.
-
동일한 데이터 속성 별로 모듈로 나누는 방법
프로젝트의 각각의 기능에 맞춰 여러 개 만들어 세분화하는 방식입니다.
const moduleA = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = createStore({ modules: { a: moduleA, b: moduleB } })
만약 먼저 간단하게 한 파일 내에서 모듈을 분리한다면 위와 같은 예제로 분리할 수 있습니다.
이러한 방식과 같이 기본적인 분리 방법을 알면 파일별로 분리하는 것도 간단합니다.namespaced를 true로 설정해야 store module 간의 중복적인 이름을 가진 속성을 사용할 때 일어나는 충돌을 막을 수 있습니다.
// store/moduleA.js export const moduleA = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } }
// store/moduleB.js export const moduleB = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { ... } }
// store/index.js import { moduleA } from './store/moduleA.js'; import { moduleB } from './store/moduleB.js'; const store = createStore({ modules: { a: moduleA, b: moduleB } }) export default store;
위에 modules에서 선언한 모듈명을 아래와 같이 component에서 사용합니다.
... setup() { const store = useStore(); const state = computed(() => store.state.moduleName.stateName); const getter = computed(() => store.getters["moduleName/getterName]); store.commit("moduleName/mutationName", params) store.dispatch("moduleName/actionName", params) }
위와 같이 컴포넌트에서 사용하면 됩니다.
※ rootState, rootGetters ※
rootState와 rootGetters는 getters나 actions의 전달인자에서 볼 수 있습니다.
이는 루트 저장소 즉, 모듈화를 했을 때 modules를 선언한 store파일의 state와 getters를 불러올 수 있습니다.위 예제 코드를 좀 변경해서 설명드리겠습니다.
// store/index.js import { moduleA } from './store/moduleA.js'; import { moduleB } from './store/moduleB.js'; const store = createStore({ state: { test: 1}, getters: { getTest: state => state.test++}, modules: { a: moduleA, b: moduleB } }) export default store;
store/index.js 파일에 state와 getter를 위와 같이 추가하고,
// store/moduleA.js export const moduleA = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { logRootData(context, payload) { console.log(context.rootState.test) // 1 console.log(context.rootGetters.getTest) // 2 } }, getters: { getRootState (state, getters, rootState) { return rootState.test }, getRootGetters (state, getters, rootState, rootGetters) { return rootGetters.getTest }, } }
store/moduleA.js 파일에서 다음과 같이 전달인자를 통해 사용할 수 있습니다.
이를 응용한다면 다른 모듈에 존재하는 state나 getter를 root store에 등록하여 사용할 수 있기 때문에 상황에 따라 유용한 기능이라고 생각합니다.
-
store의 각 속성 별로 모듈로 나누는 방법
store를 state, getters, mutations, actions 각각의 파일로 분리하는 방식입니다.
// store/state.js const state = { todoList: [ {id:1, todo: "영화보기", done: false}, {id:2, todo: "게임하기", done: true}, {id:3, todo: "공부하기", done: false}, ] }; export default state;
// store/getters.js const getters = { getList: state => state.todoList }; export default getters;
// store/mutations.js const mutations = { setList: (state, payload) => { .... } }; export default mutations;
// store/actions.js const actions = { getListData: (context, payload) => { .... } }; export default actions;
// store/index.js import stateModule from './state.js'; import gettersModule from './getters.js'; import mutationsModule from './mutations.js'; import actionsModule from './actions.js'; const store = createStore({ state: stateModule, getters: gettersModule, mutations: mutationsModule, actions: actionsModule });
컴포넌트에서의 사용방법은 동일합니다.
여기서는 기본적인 틀과 방법만 정리하였지만, 모듈화를 익히시고, 능숙하게 사용하신다면, 더욱 깔끔하고 다양하게 모듈로 분리하실 수 있습니다.
※ Vuex Helpers ※
Vuex Helpers는 options-api 방식일 때 컴포넌트 내에서 vuex를 사용할 경우에 반복되는 코드를 줄이고 코드의 효율성 및 재활용 성을 높이는 방법입니다. (composition-api에서는 helpers를 사용하는 방법이 복잡하여 외부 라이브러리를 활용해야 합니다.)
state, getters, mutations, actions 각각 helpers로 변경해서 사용하는 기본적인 방법을 예제를 통해 보여드리겠습니다.
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex'
export default {
name: 'Sample',
computed: {
//1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 state 입력
...mapState('book', {
msg: state => state.message // -> this.msg
}),
//1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 getter 입력
...mapGetters('book', [
'getMsg' // -> this.getMsg
])
},
methods: {
//1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 mutation 입력
...mapMutations('book', [
'changeMessage' // -> this.changeMessage()
]),
//1번째 전달인자는 모듈화된 store를 입력하고, 2번째 전달인자는, 사용할 action 입력
...mapActions('book', [
'callMutation' // -> this.callMutation()
])
}
}
위 코드의 옆에 각각 주석을 달아놓은 거처럼 this.{name}으로 사용하고, component에서 사용할땐 this.를 빼고 사용합니다. 단일 store 파일만 쓰는 경우에는 1번째 전달인자를 사용하지 않으면 됩니다.
createNamespacedHelpers를 사용하여 store 모듈을 미리 설정할 수 있습니다.
import { createNamespacedHelpers } from 'vuex'
const bookHelper = createNamespacedHelpers('book');
const bookListHelper = createNamespacedHelpers('bookList');
export default {
name: 'Sample',
computed: {
...bookHelper.mapState({
message: state => state.message // -> this.message
}),
...bookHelper.mapGetters([
'getMsg' // -> this.getMsg
]),
...bookListHelper.mapState({
messageList: state => state.messageList // -> this.messageList
}),
...bookListHelper.mapGetters([
'getMsgList' // -> this.getMsgList
])
},
methods: {
...bookHelper.mapMutations([
'changeMessage' // -> this.changeMessage()
]),
...bookHelper.mapActions([
'callMutation' // -> this.callMutation()
]),
...bookListHelper.mapMutations([
'changeMessageList' // -> this.changeMessageList()
]),
...bookListHelper.mapActions([
'callMutationList' // -> this.callMutationList()
]),
}
}
Leave a comment