2-5. redux-saga
redux-saga 는 비동기작업처럼, 리듀서에서 처리하면 안되는 순수하지 않은 작업들을 하기 위한 리덕스 미들웨어입니다.
redux-thunk 의 경우 함수를 dispatch 해주었고, reduix-promise-middleware 나 redux-pender 에선 Promise 가 들어있는 액션을 dispatch 해주었다면, redux-saga 에서는 일반 액션을 dispatch 하게 됩니다.
그리고, 특정 액션이 발생하면 이를 모니터링 하여 이에 기반하여 추가적인 작업을 하도록 설계합니다.
redux-saga 에서는 Generator 라는 것을 사용해서 function*
같은 문법을 사용하게 됩니다. (또 다른 참고자료)
redux-saga 의 매뉴얼의 한국어 번역본은 여기 에서 참고 하실 수 있습니다.
모듈 설치
yarn add redux-saga
redux-saga 미들웨어 적용
기존의 redux-pender 와 saga 는 비활성화 하겠습니다.
src/store.js
import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import modules from './modules';
const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(modules, applyMiddleware(logger, sagaMiddleware));
export default store;
counterSaga 만들기
비동기 카운터를 구현하기 위해 counterSaga 를 만들어보겠습니다.
src/modules/counter.js
import { delay } from 'redux-saga';
import { put, takeEvery } from 'redux-saga/effects';
import { handleActions, createAction } from 'redux-actions';
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const INCREMENT_ASYNC = 'INCREMENT_ASYNC';
const DECREMENT_ASYNC = 'DECREMENT_ASYNC';
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);
export const incrementAsync = createAction(INCREMENT_ASYNC);
export const decrementAsync = createAction(DECREMENT_ASYNC);
function* incrementAsyncSaga() {
yield delay(1000);
yield put(increment());
}
function* decrementAsyncSaga() {
yield delay(1000);
yield put(decrement());
}
export function* counterSaga() {
yield takeEvery(INCREMENT_ASYNC, incrementAsyncSaga);
yield takeEvery(DECREMENT_ASYNC, decrementAsyncSaga);
}
export default handleActions(
{
[INCREMENT]: (state, action) => state + 1,
[DECREMENT]: (state, action) => state - 1
},
1
);
- put: 새 액션을 dispatch 합니다
- delay: 작업을 지연시킵니다.
- takeEvery: 특정 액션을 모니터링 하고, 발생하면 특정 함수를 발생시킵니다.
rootSaga 만들기
우리는 여러개의 saga 를 만들고, 하나의 루트 saga 를 만들것입니다. 지금은 하나밖에 없지만 일단 다음과 같이 all 함수를 사용하여 rootSaga 를 만들어보세요.
src/modules/index.js
import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import post from './post';
import { all } from 'redux-saga/effects';
export function* rootSaga() {
yield all([counterSaga()]);
}
export default combineReducers({
counter,
post
});
pender 관련 코드를 날려주었으니, App.js 에서 pender 관련 코드를 없애주세요.
src/App.js
// ...
export default connect(
state => ({
number: state.counter,
post: state.post.data,
// loading: state.pender.pending['GET_POST'],
// error: state.pender.failure['GET_POST']
}),
dispatch => ({
CounterActions: bindActionCreators(counterActions, dispatch),
PostActions: bindActionCreators(postActions, dispatch)
})
)(App);
sagaMiddleware 에 rootSaga 적용
rootSaga 를 만드셨다면, store 를 생성한 다음에 sagaMiddleware.run 을 통하여 등록합니다.
src/store.js
import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import modules, { rootSaga } from './modules';
const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(modules, applyMiddleware(logger, sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
App 에서 increment, decrement 대체
src/App.js
<button onClick={() => CounterActions.increment()}>+</button>
<button onClick={() => CounterActions.decrement()}>-</button>
카운터가 비동기적으로 작동하나요?
postSaga 만들기
이번에는, postSaga 를 만들어보겠습니다.
src/modules/post.js
import { createAction, handleActions } from 'redux-actions';
import axios from 'axios';
import { call, put, takeEvery } from 'redux-saga/effects';
function getPostAPI(postId) {
return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`);
}
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';
export const getPost = createAction(GET_POST, postId => postId);
const something = () => ({
data: { title: 'hello', body: 'world' }
});
function* getPostSaga(action) {
console.log(call(something, ''));
try {
const response = yield call(getPostAPI, action.payload);
yield put({ type: GET_POST_SUCCESS, payload: response });
} catch (e) {
yield put({ type: GET_POST_FAILURE, payload: e });
}
}
const initialState = {
data: {
title: '',
body: ''
}
};
export function* postSaga() {
yield takeEvery('GET_POST', getPostSaga);
}
export default handleActions(
{
[GET_POST_SUCCESS]: (state, action) => {
const { title, body } = action.payload.data;
return {
data: { title, body }
};
}
},
initialState
);
- call: 첫번째 파라미터로 전달한 함수에 그 뒤에 있는 파라미터들은 전달하여 호출해줍니다. 이를 사용하면 나중에 테스트를 작성하게 될 때 용이합니다. 참고
다 작성하셨으면, 이를 rootSaga 에 포함시키세요.
src/modules/index.js
import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import post, { postSaga } from './post';
import { all } from 'redux-saga/effects';
export function* rootSaga() {
yield all([counterSaga(), postSaga()]);
}
export default combineReducers({
counter,
post
});
어떤가요? 잘 작동하나요?
정리
redux-saga 는 초반진입장벽이 조금 있는 편입니다. 무조건 써야 하는 것은 아니니, 이런게 있다는 것 정도만 알아두세요.
나중에 이 미들웨어에 대해서 더 자세히 알아보고싶다면 redux-saga 로 비동기처리와 분투하다 포스트를 읽어보시면 도움이 될 수 있습니다.