액션
생성 시점에서 커밋된 뮤테이션
앞서 다룬 뮤테이션 파트에서 컴포넌트 생성 시점에 스토어 객체의 커밋 메서드로 API의 도서 데이터를 스토어의 스테이트에 설정했었습니다.
// components/BookList.vue
export default {
name: "BookList",
computed: {
books() {
return store.getters.availableBooks;
}
},
created() {
shop.getBooks(books => {
store.commit("setBooks", books);
});
},
// ...
}
비동기 처리와 액션
위 코드는 잘 작동하지만 Vuex를 사용하는 베스트 솔루션은 아닙니다. 뮤테이션은 동기(Sync) 처리만 가능해 비동기(Async) 처리 과정을 외부에 의존하므로 유지보수 관점에서 불편합니다. (컴포넌트 마다 데이터를 불러들이는 코드가 필요)
Vuex는 이러한 문제를 해결하기 위한 방법으로 액션을 제공합니다. 액션(actions)에 등록된 메서드는 비동기 처리 후, 뮤테이션을 커밋할 수 있어 스토어에서 비동기 처리 로직을 관리할 수 있습니다.
NOTE
Vuex 스토어의 액션(actions
)는 Vue 컴포넌트의 메서드(methods
)와 유사합니다.
컴포넌트 생성 시점에서 처리했던 로직을 담당하는 fetchBooks
액션을 설정하는 과정은 다음과 같습니다.
- API를 불러들입니다.
- API의
shop.getBooks
를 통해 지연된 처리(시뮬레이션) 후,setBooks
뮤테이션을 커밋합니다.
// store/index.js
import shop from '@/api/shop'; // [1]
new Vuex.Store({
state: {
books: []
},
getters: {
availableBooks(state, getters) {
return state.books.filter(book => book.inventory > 0);
}
},
actions: {
fetchBooks(context) {
shop.getBooks(books => context.commit('setBooks', books)); // [2]
}
},
mutations: {
setBooks(state, books) {
state.books = books;
}
}
})
NOTE
액션에 등록된 메서드는 첫번째 인자로 스토어의 컨텍스트(context
)를 전달 받습니다.
context = { state, getters, commit, dispatch, ... }
NOTE
구조 분해 할당(destructuring assignment)을 활용해 컨텍스트 객체의 속성 중 일부를 빼내 사용할 수 있습니다.
actions: {
fetchBooks({commit}) {
shop.getBooks(books => commit('setBooks', books));
}
}
디스패치 액션
이제 컴포넌트 생성 시점에서 도서 데이터를 가져오는 로직 대신,
스토어 객체의 디스패치(dispatch
) 메서드를 사용해 fetchBooks
액션 실행을 요청합니다.
// components/BookList.vue
import store from '@/store';
export default {
name: "BookList",
computed: {
books() {
return store.getters.availableBooks;
}
},
created() {
store.dispatch("fetchBooks");
},
// ...
}
NOTE
스토어 객체의 dispatch
메서드는 액션 실행을 요청합니다.
store.dispatch('액션_이름'[, 페이로드])
지연된 처리 시뮬레이션
비동기 처리 로직을 시뮬레이션 테스트 하기 위해 api/shop.js
코드를 수정합니다.
// api/shop.js
export default {
getBooks (cb) {
window.setTimeout(() => cb(_books), 3000) // 3초 뒤에 cb() 실행
},
// ...
}
Vue Devtools - Vuex 탭에서 시뮬레이션 결과를 확인할 수 있습니다. 3초 뒤 UI 화면이 업데이트 됩니다.
로딩 프로세스
지연된 시간이 길어지면 사용자에게 콘텐츠를 로딩 중임을 화면에 표시하여 알려줘야 합니다.
컴포넌트 데이터 loading
을 추가한 후, 초기 값으로 false
를 설정합니다.
// components/BookList.vue
export default {
name: "BookList",
data() {
return {
loading: false
};
},
// ...
}
컴포넌트 템플릿에 로딩 스피너를 추가한 후, v-if
, v-else
디렉티브를 설정해
loading
데이터 값의 참/거짓에 따라 화면에 조건부 렌더링 되도록 설정합니다.
<!-- components/BookList.vue -->
<template>
<div class="book-list">
<h1>도서 목록</h1>
<img v-if="loading" src="https://i.imgur.com/JfPpwOA.gif" alt="로딩 중...">
<ul v-else>
<li
v-for="(book,i) in books"
:key="i"
>{{ book.name }} / {{ book.price }}</li>
</ul>
</div>
</template>
컴포넌트의 생성 시점 훅 함수 코드를 다음과 같이 수정합니다.
- 컴포넌트 생성 시점
loading
데이터 값을true
로 설정해 로딩 중으로 상태를 변경합니다. - 스토어 객체의 디스패치로 스토어의 books 스테이트가 업데이트 되면, 로딩 완료 상태로 변경합니다.
// components/BookList.vue
created() {
this.loading = true; // [1]
store.dispatch("fetchBooks")
.then(() => (this.loading = false)); // [2]
},
주의!
스토어 객체의 dispatch
메서드에 체인하여 .then()
을 수행하려면 Promise를 반환해야 합니다.
스토어의 fetchBooks
액션 메서드에 Promise 객체를 생성 반환하는 구문을 추가합니다. 도서 데이터를 커밋한 후 resolve()
가 실행되면서
컴포넌트 생성 시점에 설정한 .then()
구문이 실행됩니다.
// store/index.js
actions: {
fetchBooks({commit}) {
return new Promise((resolve, reject) => {
shop.getBooks(books => {
commit.commit("setBooks", books);
resolve();
});
});
}
},
작성된 코드의 실행 결과를 브라우저에서 살펴보면 로딩 중인 상태를 화면에 표시한 후, 로딩이 완료되면 로딩 이미지를 감추고 콘텐츠가 표시됩니다.
NOTE
setTimeout
시뮬레이션 대신, Ajax 비동기 통신을 수행해
도서 데이터를 불러올 수 있습니다.
fetch API를 사용할 경우
// api/shop.js
export default {
getBooks (cb) {
fetch('https://api.myjson.com/bins/18zxeg')
.then(response => response.json())
.then(data => cb(data));
},
}
axios 라이브러리를 사용할 경우
$ yarn add axios # 또는 npm i axios
// api/shop.js
import axios from 'axios';
export default {
getBooks (cb) {
axios.get('https://api.myjson.com/bins/18zxeg')
.then(res => cb(res.data));
},
}