흐름
'비동기' 액션 타입 선언
→ INCREASE_ASYNC
, DECREASE_ASYNC
액션 생성 함수 제작 (해당 액션에 대한)
increaseAsync
, decreaseAsync
saga(제너레이터 함수) 제작
코드 (
modules/counter.js
)
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);
// 마우스 클릭 이벤트가 payload 안에 들어가지 않도록
// () => undefined를 두 번째 파라미터로 넣음.
**export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);**
**function* increaseSaga() {
yield delay(1000); // 1초를 기다림
yield put(increase()); // 특정 액션을 디스패치함.
}
function* decreaseSaga() {
yield delay(1000); // 1초를 기다림.
yield put(decrease()); // 특정 액션을 디스패치함.
}**
**export function* counterSaga() {
// takeEvery: 들어오는 모든 액션에 대해 특정 작업 처리
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// takeLatest: 가장 마지막 실행된 작업만 수행. (기존 진행 중이던 작업 있으면 취소 처리.)
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}**
// 초기 값
const initialState = 0; // 상태는 꼭 객체일 필요 x. 숫자도 작동 가능
// 리듀서
const counter = handleActions(
{
[INCREASE]: (state) => state + 1,
[DECREASE]: (state) => state - 1,
},
initialState,
);
export default counter;
코드 (
modules/index.js
)
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import counter, **{ counterSaga }** from './counter';
import sample from './sample';
import loading from './loading';
const rootReducer = combineReducers({
counter,
sample,
loading,
});
**export function* rootSaga() {
// all 함수: 여러 사가 합치는 역할
yield all([counterSaga()]);
}**
export default rootReducer;
코드 (
index.js
)
// (나머지 생략)
import rootReducer, **{ rootSaga }** from './modules';
**import createSagaMiddleware from 'redux-saga';**
**const sagaMiddleware = createSagaMiddleware();**
const store = createStore(
rootReducer,
applyMiddleware(logger, ReduxThunk, **sagaMiddleware**),
);
**sagaMiddleware.run(rootSaga);**
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
코드 (
App.js
)
import React from 'react';
**import CounterContainer from './containers/CounterContainer';**
const App = () => {
return (
<div>
**<CounterContainer />**
</div>
);
};
export default App;
컨테이너 컴포넌트 내부 → 수정 없는 이유
해당 기능의 리덕스 모듈이 변경되었지만, 기존에 사용 중인 액션 생성 함수와 이름 동일함.
반복 코드 함수화 → 액션 타입, 디스패치, loading 처리, API 호출 (lib/createRequestSaga.js
)
modules/sample.js
import { createAction, handleActions } from 'redux-actions';
**import { takeLatest } from 'redux-saga/effects';**
import * as api from '../lib/api';
**import createRequestSaga from '../lib/createRequestSaga';**
// 액션 타입 선언
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
**// 액션 생성 함수
export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);**
**// createRequestSaga에 분리:** 액션 타입, API 호출, loading 메소드
**const getPostSaga = createRequestSaga(GET_POST, api.getPost);
const getUsersSaga = createRequestSaga(GET_USERS, api.getUsers);**
**// Redux-saga**
**export function* sampleSaga() {
yield takeLatest(GET_POST, getPostSaga);
yield takeLatest(GET_USERS, getUsersSaga);
}**
// 초기 상태 선언
// 요청의 '로딩 중' 상태 관리 -> loading 객체
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;
lib/createRequestSaga.js
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)); // 로딩 시작
// 파라미터로 action을 받아 오면 액션의 정보 조회 가능
try {
// call 사용 시, Promise 반환 함수 호출하고 기다릴 수 있음.
const reponse = yield call(request, action.payload); // 의미: request(action.payload)
yield put({
type: SUCCESS,
payload: reponse.data,
});
} catch (e) {
// try/catch 문 사용하여 에러 잡기 가능
yield put({
type: FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoading(type)); // 로딩 끝
};
}
참고
요청에 필요한 값 전송하는 법
경우 예시
GET_POST 액션의 경우 → api요청 시 어떤 id로 조회할지 정해 주어야 함.
액션의 payload: 요청에 필요한 값
사가(액션 처리 하기 위한) 작성 시
→ API 호출 함수의 인수: payload값
API 호출 시
call
함수 사용 (사가 내부 직접 호출 x)코드 (
modules/index.js
)
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,
sample,
loading,
});
export function* rootSaga() {
// all 함수: 여러 사가 합치는 역할
yield all([counterSaga(), **sampleSaga()**]);
}
export default rootReducer;
App.js
import React from 'react';
import SampleContainer from './containers/SampleContainer';
const App = () => {
return (
<div>
<SampleContainer />
</div>
);
};
export default App;
어떤 액션이 디스패치 되고 있는지 확인 → 편리
$ yarn add redux-devtools-extension
index.js
)composeWithDevTools
→ 리덕스 미들웨어와 함께 사용 시
applyMiddleware
부분 감싸기
코드
// (생략)
**import { composeWithDevTools } from 'redux-devtools-extension';**
const store = createStore(
rootReducer,
**composeWithDevTools(**applyMiddleware(logger, ReduxThunk, sagaMiddleware)**)**,
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
참고