😎 비동기 작업을 처리하는 Redux-Saga
- redux-thunk 다음으로 가장 많이 사용되는 미들웨어
- 특정 액션이 디스패치 되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성
😁 Redux-Saga가 필요한 상황
- 기존 요청을 취소 처리해야 할 때(불필요한 중복 요청 방지)
- 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
- 웹소켓을 사용할 때
- API 요청 실패 시 재요청해야 할 때
🤩 제너레이터 함수 이해하기
- redux-saga에서는 ES6의 제너레이터 함수라는 문법을 사용한다.
- 제너레이터 함수는 함수에서 값을 순차적으로 반환할 수 있다.
- 제너레이터 함수를 만들 때는 function* 키워드를 사용한다.
- 제너레이터 함수에서 반환값은 return이 아닌 yield를 사용한다.
- 호출할 때는 함수를 실행시켜 변수에 할당하고 그 함수에서 next 메소드를 실행시켜 부른다.
- next 메소드에 파라미터를 넣으면 제너레이터 함수에서 yield를 사용하여 해당 값을 조회할 수 있다.
- 제너레이션 함수를 여기서는 사가(saga)라고 부른다.
🥳 실전 코드 연습
- 아래의 코드는 redux-thunk 기반 위에 덮어쓰거나 새로 만들면 된다.
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer, { rootSaga } from "./modules";
import { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";
import creasteSagaMiddleware from "redux-saga";
import { composeWithDevTools } from "redux-devtools-extension";
const rootElement = document.getElementById("root");
const logger = createLogger();
const sagaMiddleware = creasteSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(logger, ReduxThunk, sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
rootElement
);
import { combineReducers } from "redux";
import { all } from "redux-saga/effects";
import counter, { counterSaga } from "./counter";
import sample, { sampleSaga } from "./sample";
import loading from "./loading";
const rootReducer = combineReducers({
counter,
loading,
sample
});
export function* rootSaga() {
yield all([counterSaga(), sampleSaga()]);
}
export default rootReducer;
import { createAction, handleActions } from "redux-actions";
import {
delay,
put,
takeEvery,
takeLatest,
select,
throttle
} from "redux-saga/effects";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
const INCREASE_ASYNC = "counter/INCREASE_ASYNC";
const DECREASE_ASYNC = "counter/DECREASE_ASYNC";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);
function* increaseSaga() {
yield delay(1000);
yield put(increase());
const number = yield select((state) => state.counter);
console.log(`현재 값은 ${number}입니다`);
}
function* decreaseSaga() {
yield delay(1000);
yield put(decrease());
}
export function* counterSaga() {
yield throttle(3000, INCREASE_ASYNC, increaseSaga);
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
const initialState = 0;
const counter = handleActions(
{
[INCREASE]: (state) => state + 1,
[DECREASE]: (state) => state - 1
},
initialState
);
export default counter;
import { createAction, handleActions } from "redux-actions";
import * as api from "../lib/api";
import { call, put, takeLatest } from "redux-saga/effects";
import { startLoading, finishLoading } from "./loading";
import createRequestSaga from "../lib/createRequestSaga";
const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_USERS = "sample/GET_USER";
const GET_USERS_SUCCESS = "sample/GET_USER_SUCCESS";
export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);
const getPostSaga = createRequestSaga(GET_POST, api.getPost);
const getUsersSaga = createRequestSaga(GET_USERS, api.getUsers);
export function* sampleSaga() {
yield takeLatest(GET_POST, getPostSaga);
yield takeLatest(GET_USERS, getUsersSaga);
}
const initialState = {
post: null,
users: null
};
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload
})
},
initialState
);
export default sample;
import { createAction, handleActions } from "redux-actions";
const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";
export const startLoading = createAction(
START_LOADING,
(requestType) => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
(requestType) => requestType
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false
})
},
initialState
);
export default loading;
import { call, put } from "redux-saga/effects";
import { startLoading, finishLoading } from "../modules/loading";
export default function createRequestSaga(type, request) {
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return function* (action) {
yield put(startLoading(type));
try {
const response = yield call(request, action.payload);
yield put({
type: SUCCESS,
payload: response.data
});
} catch (e) {
yield put({
type: FAILURE,
payload: e,
error: true
});
}
yield put(finishLoading(type));
};
}