Vue : Vuex

1. Vuex란?

  • Store라고도 불림.
  • 무수히 많은 컴포넌트의 데이터를 관리하기 위한 상태 관리 패턴이자 라이브러리
    • 서비스가 복잡해질수록 컴포넌트 간에 데이터 전달이 어려워지기 때문에 Vuex를 사용하는 것
  • React의 Flux 패턴에서 기인

Flux 패턴 ?

기존 MVC 패턴은 Model과 View 간 양방향 통신이 가능합니다. 하지만 서비스가 커질 수록 이런 양방향 통신이 데이터의 흐름을 예측할 수 없게 만듭니다.

때문에 이런 단점을 극복하고자 데이터의 흐름이 여러 갈래로 나뉘지 않고 단방향으로만 처리하는 Flux 패턴이 나오게됩니다.

2. Flow

Vuex의 Flow는 아래와 같이 이루어집니다.

  • 컴포넌트 -> 비동기 로직 -> 동기 로직 -> 상태 변경

3. 설치 및 등록

  1. Vuex를 사용할 프로젝트로 이동
  2. 터미널에서 npm i vuex@3.6.2 –save (2022.02.07 부터 이 명령어로 사용)
  3. store 폴더 생성

store 폴더 ?

Vuex 폴더는 보통 store라는 이름으로 만듭니다. 해당 폴더는 관행적으로 프로젝트의 src 바로 아래에 store 폴더를 만듭니다.

  1. store 폴더에 store.js 생성
  2. 아래의 세팅을 참고

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;

참고 : Vue.js 중급 강좌 - 웹앱 제작으로 배워보는 Vue.js, ES6, Vuex

댓글남기기