모듈

스토어를 여러 파일로 분할하는 것만으로는 충분하지 않은 경우, 스토어를 Vuex 모듈로 나눠 관리 할 수 있습니다.

NOTE

개별 Vuex 모듈은 로컬 상태, 뮤테이션, 액션, 게터를 가질 수 있습니다.

모듈 분리

스토어에서 books 모듈과 관련된 것을 추려내어 books.js 모듈 파일을 만듭니다.

// store/modules/books.js

import shop from '@/api/shop';

export default {
  state: {
    books: [],
  },
  getters: {
    availableBooks(state) {
      return state.books.filter(book => book.inventory > 0)
    },
    checkOutOfStock(state) {
      return book => {
        return book.inventory === 0;
      }
    },
  },
  actions: {
    fetchBooks({commit}) {
      return new Promise((resolve, reject) => {
        shop.getBooks(books => {
          commit("setBooks", books)
          resolve();
        });
      });
    },
  },
  mutations: {
    setBooks(state, books) {
      state.books = books
    },
    decreamentBookInventory(state, book) {
      book.inventory--;
    },
  }
}

cart 모듈 또한 추려내어 cart.js 파일을 생성합니다.

// store/modules/cart.js

import shop from '@/api/shop';

export default {
  state: {
    cart: [],
    purchageStatus: '',
  },
  getters: {
    cartBooks(state) {
      return state.cart.map(cartItem => {
        const book = state.books.find(book => book.id === cartItem.id);
        return {
          name: book.name,
          price: book.price,
          quantity: cartItem.quantity
        }
      });
    },
    cartTotal(state, getters) {
      return getters.cartBooks
        .reduce((total,cartItem) => total + cartItem.price * cartItem.quantity, 0);
    }
  },
  actions: {
    addBookToCart({state, getters, commit}, book) {
      if (!getters.checkOutOfStock(book)) {
        const cartItem = state.cart.find(item => item.id === book.id);
        if (!cartItem) {
          commit('pushBookToCart', book.id);
        } else {
          commit('increamentBookQuantity', cartItem);
        }
        commit('decreamentBookInventory', book);
      }
    },
    purchage({state, commit}) {
      shop.buyBooks(
        state.cart,
        () => {
          commit('emptyCart');
          commit('notifyStatus', '성공');
        },
        () => {
          commit('notifyStatus', '실패');
        }
      );
    },
  },
  mutations: {
    emptyCart(state) {
      state.cart = [];
    },
    notifyStatus(state, status) {
      state.purchageStatus = status;
    },
    pushBookToCart(state, bookId) {
      state.cart.push({
        id: bookId,
        quantity: 1
      })
    },
    increamentBookQuantity(state, cartItem) {
      cartItem.quantity++;
    },
  }
}

분리 관리되는 각 모듈을 스토어에서 modules 속성에 설정합니다.





 
 




 
 
 


// store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import books from './modules/books';
import cart from './modules/cart';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    books, cart
  }
});

모듈 분리에 따른 코드 리팩토링

모듈을 분리한 후, Vue Devtools 개발 도구 Vuex 탭을 살펴보면 스토어에 병합된 모듈의 각 스테이트는 모듈 객체에 종속되어 있는 것을 볼 수 있습니다. 분리된 모듈은 로컬 스테이트를 가지기 때문입니다.

store.state.books.books

이러한 이유로 컴포넌트가 스토어의 각 스테이트를 올바로 찾지 못해 애플리케이션이 정상적으로 작동하지 않게 됩니다. 스토어의 스테이트를 올바로 접근할 수 있도록 코드를 변경해야 정상 작동됩니다.

// components/BookList.vue

export default {
  name: 'BookList',
  computed: {
    ...mapState({
      books: state => state.books.books
    }),
  },
  // ...
}

그런데 state.books.books는 이름이 중복적인 느낌이 강하니 books.js 모듈을 수정해봅니다.







 




 






 






// store/modules/books.js

import shop from '@/api/shop';

export default {
  state: {
    items: [],
    // books: []
  },
  getters: {
    availableBooks(state) {
      return state.items.filter(book => book.inventory > 0)
      // return state.books.filter(book => book.inventory > 0)
    }
  },
  // ...
  mutations: {
    setBooks(state, books) {
      state.items = books
      // state.books = books
    },
    // ...
  }
};

앞서 중복되었던 이름 대신, state.books.items를 사용해 이질감을 줄일 수 있습니다.







 






// components/BookList.vue

export default {
  name: 'BookList',
  computed: {
    ...mapState({
      books: state => state.books.items
      // books: state => state.books.books
    }),
  },
  // ...
}

cart.js 또한 동일한 방법으로 수정합니다.





 





 







 






 







 




 






// store/modules/cart.js

export default {
  state: {
    items: [],
    // cart: [],
    purchageStatus: '',
  },
  getters: {
    cartBooks(state, getters) {
      return state.items.map(cartItem => {...});
      // return state.cart.map(cartItem => { ... });
    },
    // ...
  },
  actions: {
    addBookToCart({state, getters, commit}, book) {
      if (!getters.checkOutOfStock(book)) {
        const cartItem = state.items.find(item => item.id === book.id);
        // const cartItem = state.cart.find(item => item.id === book.id);
        // ...
      }
    },
    purchage({state, commit}) {
      shop.buyBooks(
        state.items,
        // state.cart,
        // ...
      );
    },
  },
  mutations: {
    emptyCart(state) {
      state.items = [];
      // state.cart = [];
    },
    // ...
    pushBookToCart(state, bookId) {
      state.items.push({...});
      // state.cart.push({ ... });
    },
    // ...
  }
}

쇼핑 카트에 추가 시 오류 발생

화면에 출력된 도서 목록의 '카트에 추가' 버튼을 누르니 다음과 같은 오류가 발생합니다.

오류 발생!

Cannot read property 'find' of undefined

해당 오류는 사용자가 선택한 도서를 쇼핑 카트에 추가할 때 cart모듈의 cartBooks 게터가 books 모듈에 접근해야 하는데 cart 모듈의 state는 로컬 스테이트로 books 모듈에 접근할 수 없어 발생한 오류입니다.

이 문제를 해결하려면 스토어의 루트 스테이트를 사용해야 합니다. Vuex 게터는 3번째 인자로 rootState를 전달 받습니다. 이를 통해 books에 접근하면 문제가 해결됩니다.






 




// store/modules/cart.js

getters: {
  cartBooks(state, getters, rootState) {
    return state.items.map(cartItem => {
      const book = rootState.books.items.find(book => book.id === cartItem.id);
      // const book = state.books.find(book => book.id === cartItem.id);
      return { ... }
}