redux-saga는 generator를 사용한 react 상태관리 툴로서 redux-saga 에서는 다음과 같이 정의하고 있다.
redux-saga는 사이드 이펙트(예를 들어, 데이터 불러오기 같은 비동기적인 것들 그리고 브라우저 캐시에 접근하는 것과 같이 순수하지 않은 것들)를 만들어내는데 react/redux 애플리케이션에서 더 쉽고 더 편하게 해주는데 중점을 둔 라이브러리입니다.
redux-saga는 "Task"라는 개념을 Redux로 가져오기위한 지원 라이브러리이다. 여기서 말하는 Task란 일의 절차와 같은 독립적인 실행 단위로써, 각각 평행적으로 작동한다. redux-saga는 이 Task의 실행환경을 제공한다. 더불어 비동기처리를 Task로써 기술하기 위한 준비물인 "Effect"와 비동기처리를 동기적으로 표현하는 방법을 제공하고 있다. Effect란 Task를 기술하기 위한 커맨드(명령, Primitive)와 같은 것으로, 예를들면 다음과 같은 것들이 있다.
/*비동기 액션타입 생성자*/
export const asyncActionTypeCreator = ( prefix ) => {
const asyncTypeAction = ['_INDEX','_REQUEST','_SUCCESS','_FAILURE'];
const actionNameIndex = asyncTypeAction[0];
const actionNameRequest = asyncTypeAction[1];
const actionNameSuccess = asyncTypeAction[2];
const actionNameFailure = asyncTypeAction[3];
return {
'INDEX': prefix+actionNameIndex,
'REQUEST': prefix+actionNameRequest,
'SUCCESS': prefix+actionNameSuccess,
'FAILURE': prefix+actionNameFailure,
}
};
/*비동기 액션 생성자*/
export function asyncActionCreator(actions) {
let actionCreator = createAction(actions.INDEX);
actionCreator.request = createAction(actions.REQUEST);
actionCreator.success = createAction(actions.SUCCESS);
actionCreator.failure = createAction(actions.FAILURE);
return actionCreator
}
위와 같이 액션 생성자 함수를 구성한다
export const POST_UPSERT = asyncActionTypeCreator('Post_upsert_postUpsert');
export const upsertPost = asyncActionCreator(POST_UPSERT);
redux-saga를 비롯한 모든 비동기 미들웨어의 공통점은 비동기를 요청 후 성공 및 실패 의 분기처리를 하는 것이다. 또한 API를 요청하기 전에 로딩 이미지 출력 등에 대한 작업들이 전부 각 API 호출마다 작성하게 되면 중복되는 부분이 많기 때문에 공통 함수를 작성했다.
export function * sagaApi(asyncFunc, apiFunc, apiFuncParam, successFunc, failureFunc) {
yield put(togglePageLoading(true)); // 로딩바 시작
yield put(asyncFunc.request()); // 요청대기
try {
const result = yield call(apiFunc,apiFuncParam); // 비동기처리 promise
if( result.data.code === 'SUCCESS' ){ // 요청 API 응답 코드 실패여부
yield put(togglePageLoading(false)); // 로딩바 중지
yield put(asyncFunc.success(result.data)); // 비동기 처리 성공
yield call(successFunc, result.data); // API 요청 성공 이후 작업
}else{
yield put(togglePageLoading(false)); // 로딩바 중지
yield put(asyncFunc.failure(result)); // 비동기 처리 실패(API 응답 불일치)
yield call(failureFunc(result.data)); // API 요청 실패 이후 작업
}
} catch(error) { // API 요청 자체 실패
yield put(togglePageLoading(false)); // 로딩바 중지
yield put(asyncFunc.failure(error)); // 비동기 처리 실패
yield call(failureFunc({ // API 요청 실패 이후 작업
message: error
}));
}
}
위와 같이 구성하면 API에 대한 에러 및 예상치 못한 response 에 대한 공통 처리 와 로딩 이미지 와 같은 공통화의 처리가 해당 함수 하나에서만 작업되면 되기 때문에 몹시 간결하다. 각 파라미터에 대한 설명은 다음과 같다.
// postUpsertReducer.js
...
export const PostUpsertReducer = handleActions({
[action.upsertPost.request]: (state, action) => {
return { ...state };
},
[action.upsertPost.success]: (state, action) => {
return { ...state,
postNo: initialState.postNo,
category: initialState.category,
title: initialState.title,
content: initialState.content };
},
[action.upsertPost.failure]: (state, action) => {
return { ...state, error: true, errorMsg: action.payload.message };
},
}, initialState);
위의 2번에서 설명한 비동기 공통 함수를 사용한 saga 예시이다.
API 함수
// BlogApi
export const upsertPost = (postData) => { // 게시글 Upsert 작업
return axios.post(`${BLOG_API}/post`,postData);
};
공통 함수 적용 saga 예시
//postSaga.js
function* postUpsertSaga(info) {
yield call(sagaApi, PostUpsertAction.upsertPost, BlogApi.upsertPost, info.payload,
function* success(success) {
const {postNo, categoryNo} = yield success.data;
yield put(PostsAction.getPosts(categoryNo));
yield goPostDetailPage(categoryNo, postNo); // 게시글 자세히 보기
},
function* failure(error) {
const { message } = yield error;
yield alert(message !== undefined ? message : '게시글 편집 실패.');
});
}
export default function* root() {
yield all([
takeLatest(PostUpsertAction.POST_UPSERT.INDEX, postUpsertSaga) // asyncCall
]);
}
사용법은 해당 공통 saga를 묶기 위한 call을 사용해서 호출하면 된다. 함수가 추가되면 root에 각 saga 함수를 추가해 주면된다.
takeLatest 를 사용한 것은 가장 마지막에 발생한 요청에 대한 응답을 얻기 위함인데 이부분은 redux-saga에서 제공하는 helper effects를 참조하도록 하자.
각 파라미터는 3번에서 설명한 것처럼 넣어주면 된다. 위의 게시글 Upsert의 flow는 다음과 같다.
upsertPost를 호출해 게시글 Upsert 작업 시작
게시글 Upsert 작업이 성공
api 결과값에 있는 게시글 번호와 카테고리 번호로 게시글 자세히 보기 호출
실패 할 경우 편집 실패 메세지. 발생
위 의 폴더 구성으로 index.js 에 폴더를 묶기 위해 index.js에서 각 saga를 묶어줘야 한다.
index.js 구성
import { fork, all } from 'redux-saga/effects';
import PostListSaga from './PostListSaga';
import PostInfoSaga from './PostInfoSaga';
import PostUpsertSaga from './PostUpsertSaga';
import CategorySaga from './CategorySaga';
import AuthSaga from './AuthSaga';
export default function* rootSaga() {
yield all([
fork(PostListSaga),
fork(PostInfoSaga),
fork(PostUpsertSaga),
fork(CategorySaga),
fork(AuthSaga)
])
}
import { fork, all } from 'redux-saga/effects';
// imports all file except index.js
const req = require.context('.', true, /^(?!.\/index).*.js$/);
const sagas = [];
req.keys().forEach((key) => {
sagas.push(req(key).default);
});
export default function* rootSaga() {
yield all(
sagas.map(saga=>fork(saga))
)
}
후자의 경우 해당 폴더구성 depth에서 index.js 를 전부 import 시켜서 일괄로 구성하기 때문에 다른 saga 파일이 추가되어도 index.js 에 따로 추가 구성 할 필요가 없는 장점이 있다.