참고한 문서는 다음 두 개!
공식 홈페이지: AsyncActions
깃허브 공식 예제: ng-redux/examples/async
redux-thunk 의 아이디어는 다음과 같다.
lazy-evaluation of action!
즉, 객체를 리턴하는 대신 객체를 리턴하는 함수를 리턴해서 함수가 비동기적으로 작업을 진행하는 도중에 중간중간 객체를 리턴하자는 심플한 아이디어! 그럼 원래 액션객체를 받아먹던 redux reducer가 놀랄(?) 수도 있으니 그 중간에서 redux-thunk가 middleware로 중재해주겠다는 것.
그럼 결국 이 포스팅의 남은 목적은 어디서부터 angular의 dependency injection을 사용하고, 어디까지 es6의 import를 사용할 것인가의 문제로 수렴한다. 일단 대부분의 async action들은 $http
서비스가 필요하다. $ngRedux.dispatch
함수도 필요하고, 그 외에도 프로젝트에서 쓰고 있는 각종 상수들을 담고 있는 임의의 서비스 Config
도 필요하다고 하자.
아래 설명은 잘못된 구조라서 포스트를 발행한 이후에 비공개로 설정했다.
왜 잘못된 구조였는지 다음 <hr>
뒤에 이어서 설명함!
어디가 잘못됐는지 찾아보라는 뜻ㅎ
dispatch
정도야 thunk가 반환하는 함수의 인자로 넣어주면 크게 문제되진 않는다. 그 정도 수고는 별거 아닌 것 같아. 하지만, 그 때 그 때 thunk에 맞는 angular service들을 thunk를 호출할 때 마다 넣어주는 것은 너무 번거롭다고 느꼈다. 그래서 thunk를 angular.service() 의 생성자 함수 안에 넣는 것이 베스트라고 판단하게 된 것!
import angular from 'angular';
const REQUEST_EXCHANGE_RATES = 'REQUEST_EXCHANGE_RATES';
const SUCCESS_EXCHANGE_RATES = 'SUCCESS_EXCHANGE_RATES';
const FAILURE_EXCHANGE_RATES = 'FAILURE_EXCHANGE_RATES';
export const rateTypes = {
REQUEST_EXCHANGE_RATES,
SUCCESS_EXCHANGE_RATES,
FAILURE_EXCHANGE_RATES,
};
export default angular.module('olaf.service', [])
.service('GeneralService', function($http, $ngRedux, Config) {
'ngInject';
const self = this;
const dispatch = $ngRedux.dispatch;
self.getExchangeRates = () => {
const url = Config.BASE_URL + '/v1/rate';
// return $http.get(url, { cache: true }, Config.HTTP_HEADER_CONFIG);
function requestExchangeRates() {
return {
type: REQUEST_EXCHANGE_RATES,
payload: null,
};
}
function successExchangeRates(data) {
return {
type: SUCCESS_EXCHANGE_RATES,
payload: {
data
}
}
}
function failureExchangeRates(data) {
return {
type: FAILURE_EXCHANGE_RATES,
payload: {
data
}
}
}
return () => {
dispatch(requestExchangeRates());
return $http.get(url, { cache: true }, Config.HTTP_HEADER_CONFIG)
.then(
res => dispatch(successExchangeRates(res.data)),
res => dispatch(failureExchangeRates(res.data))
);
};
};
})
.name;
이렇게 선언해준 뒤에 이후 컨트롤러의 생성자 함수내부에서, 또는 mapStateToTarget()
내부에서 GeneralService.getExchangeRates()();
와 같이 currying 해주면 된다.
그 결과로 콘솔에서 위와 같이 action이 찍히는 것을 볼 수 있다!
여기서부터 수정된 구조!
위 코드는 정말 잘 작동하는데 정말 마음 껏 비웃음 당해도 할 말 없는 부분이 있으니, 한 달 이불킥 감인 ‘currying 해주면 된다’ 라는 문장이다. 여기서 다들 마음껏 웃어주시면 됨미다 ^00^ 어하ㅓㅏ하하ㅎㅎ..
여기까지 짜놓고 보니, redux-thunk가 없어도 되겠더라고. 즉, thunk를 전혀 쓰지 않고 구현한 예제라는 것이 일단 첫 번째 문제. 그리고 내가 만든 api service와 ngRedux의 결합도가 너무 높다는 것도 또 문제였다. api 호출하고 프로미스만 리턴하는 게 딱 적당한 수준인데, 여기서 action까지 뱉어낸다? 이게 두 번째 문제.
그럼 어째. 일단, state를 변경할 수 있는 건 반드시 그에 상응하는 action을 받아먹은 reducer에서 이뤄져야 한다는 것이 flux 구조니, action이 적당히 바뀌어야 하고, 그 적당히 바뀌어야 하는 부분을 바로 redux-thunk가 도와주는 것임을 다시 한 번 상기해봤다. 그리고 또한 angular의 dependency injection이 무조건 필요하다는 것도 잊지 말자.
그럼 redux와 angular가 이어지는 부분은 어디지? 컴포넌트의 생성자 함수에서 $ngRedux 를 주입받아서 action을 컨트롤러에 connect 해주는 부분이다. 이 action을 angular service로 만드는 것이 해답이었다.
OlafService
와 같은 angular service 에서 api 호출하는 promise를 리턴하고, 그것을 주입받는 OlafAction
과 같은 angular service를 만들고, 이것을 컴포넌트의 컨트롤러에서 주입받아서 connect의 두 번째 인자로 넘겨주는 것!
function olafActionConstructor(OlafService) {
'ngInject';
const getExchangeRates = () => {
return (dispatch) => {
dispatch(requestExchangeRates());
OlafService.getExchangeRates()
.then(
res => dispatch(successExchangeRates(res.data)),
res => dispatch(failureExchangeRates(res.data))
)
};
}
return {
aaa,
bbb,
ccc,
ddd,
getExchangeRates,
};
}
export default angular.module('OlafApp')
.factory('OlafAction', olafActionConstructor)
.name;
위와 같이 OlafAction을 만들어서, 컨트롤러에 connect 해준 뒤에 this.getExchangeRates();
를 불러주면 아주 잘 작동한다. 내부에서 쓰인 이 angular service에서 쓰인 [request|success|failure]ExchangeRates() action creator들은 모두 syncronous한 녀석들. async한 녀석들만 생성자 함수 안에 넣어주고, 이 녀석들 안에서 sync한 녀석들을 불러주는 형식으로 개념적 모듈화를 달성해볼까 한다.