[React Native] redux-saga 설명 및 예제
❐ redux-saga 이란?
redux-saga
는 애플리케이션의 사이드 이팩트를 더 쉽게 관리하고, 더 효율적으로 실행하고, 더 쉽게 테스트하고, 오류를 더 잘 처리하도록 하는 것을 목표로 하는 라이브러리입니다.
Action을 구독하는 Watcher, 실제 작업을 수행하는 Worker로 구성되어있습니다.
특정 액션을 모니터링하고 있다가 해당 액션이 발생하면 제너레이터 함수를 실행하여 비동기 작업을 처리한 후 액션을 디스패치합니다.
❐ redux-saga/effects 함수
redux-saga/effects는 미들웨어에 의해 수행되는 명령을 담고 있는 javascript 객체입니다.
all()
여러개의 사가를 묶어줍니다.put()
특정 액션을 디스패치합니다.
ex) yield put({ type: 'LOG_IN_SUCCESS', data: result.data });take()
해당 액션이 dispatch되면 제너레이터를 next한다.
ex) yield take('LOG_IN', logIn);takeLatest()
특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만 처리합니다.
예를 들어서 특정 액션을 처리하고 있는 동안 동일한 타입의 새로운 액션이 디스패치되면 기존에 하던 작업을 무시 처리하고 새로운 작업을 시작합니다.
ex) yield takeLatest(DECREASE3_ASYNC_ACTION, decreaseSaga);takeEvery()
특정 액션 타입에 대하여 디스패치된 모든 액션을 처리합니다.
ex) yield takeEvery(INCREASE3_ASYNC_ACTION, increaseSaga);call()
함수를 동기적으로 실행합니다.
ex) yield call(fn, action.payload)fork()
함수를 비동기적으로 실행합니다.
ex) yield fork(childTask);race()
2개의 작업을 수행할 것이지만 하나가 끝나면 다른 작업을 자동으로 취소합니다.
ex) yield race({
task: call(timerTickWorkerSaga, ...args),
cancel: take(STOP)
})join()
다른 task의 종료를 기다립니다.delay()
설정된 시간만큼 지연시킵니다.cancel()
fork()된 task를 취소시킵니다.select()
특정 상태(state)를 가져옵니다.
ex) yield select(activeUserSelector);
❐ Package 설치
redux-saga package를 설치합니다.
yarn add redux-saga
❐ 예시
예시 설명
아래와 같은 로직을 redux-saga 사용하여 변경해봅시다.
- + 버튼을 클릭하면 number가 +1 증가
- 버튼을 클릭하면 number가 -1 감소 - + 버튼을 빠르게 여러번 클릭하면 클릭한 횟수만큼 number가 증가합니다. (takeEvery : 모든 액션 처리)
- - 버튼을 빠르게 여러번 클릭하면 number가 -1만 감소합니다. (takeLatest : 가장 마지막 액션만 처리)
예시 코드
counter3.js 파일 작성
INCREASE3_ASYNC, DECREASE3_ASYNC 사가 액션을 정의합니다.
INCREASE3_ASYNC_ACTION, DECREASE3_ASYNC_ACTION 사가 액션 생성 함수를 정의합니다.
increaseSaga(), decreaseSaga() Worker Saga를 정의합니다.
counterSaga() Watcher Saga를 정의합니다.
handleActions() 함수를 이용하여 counter3Reducer 리듀서를 정의합니다.
javascriptimport {createAction, handleActions} from 'redux-actions';import {delay, put, takeEvery, takeLatest} from 'redux-saga/effects';//Action 타입 정의const INCREASE3 = 'counter/INCREASE3';const DECREASE3 = 'counter/DECREASE3';//Saga Action 타입 정의const INCREASE3_ASYNC = 'counter/INCREASE3_ASYNC';const DECREASE3_ASYNC = 'counter/DECREASE3_ASYNC';//Action 생성 함수 정의export const INCREASE3_ACTION = createAction(INCREASE3);export const DECREASE3_ACTION = createAction(DECREASE3);export const INCREASE3_ASYNC_ACTION = createAction(INCREASE3_ASYNC);export const DECREASE3_ASYNC_ACTION = createAction(DECREASE3_ASYNC);//Worker Saga 정의//put : 특정 액션을 디스패치 합니다.//takeEvery : 모든 액션을 처리합니다.//takeLatest : 가장 마지막으로 디스패치된 액션을 처리합니다.function* increaseSaga() {console.log('3. increaseSaga 호출');yield delay(1000);yield put(INCREASE3_ACTION());}function* decreaseSaga() {console.log('3. decreaseSaga 호출');yield delay(1000);yield put(DECREASE3_ACTION());}//Watcher Saga 정의export function* counterSaga() {console.log('3. counterSaga 호출');yield takeEvery(INCREASE3_ASYNC_ACTION, increaseSaga);yield takeLatest(DECREASE3_ASYNC_ACTION, decreaseSaga);}//State 초기값 정의const initialState = {number: 0,};//Reducer 정의const counter3Reducer = handleActions({[INCREASE3]: (state, action) => {console.log('3. counter3 Reducer 호출');console.log(' [parameter] previousState : ', state);console.log(' [parameter] action : ', action);let newState = {...state, number: state.number + 1};console.log(' [return] newState : ', newState);return newState;},[DECREASE3]: (state, action) => {console.log('3. counter3 Reducer 호출');console.log(' [parameter] previousState : ', state);console.log(' [parameter] action : ', action);let newState = {...state, number: state.number - 1};console.log(' [return] newState : ', newState);return newState;},},initialState,);export default counter3Reducer;
rootReducer.js 파일 작성
위에서 생성한 counter3Reducer 리듀서를 combineReducers() 함수 안에 추가합니다.
all() 함수를 이용하여 여러개의 사가를 합쳐 rootSaga를 생성해줍니다.
javascriptimport {combineReducers} from 'redux';import counter3Reducer, { counterSaga } from "./counter3";import {penderReducer} from 'redux-pender';import promiseDataReducer from './promiseData';import { all } from 'redux-saga/effects';//combineReducers() 함수를 이용하여 여러개의 Reducer를 합칩니다.//합쳐진 Reducer를 rootReducer라고 부릅니다.const rootReducer = combineReducers({//other reducers...counter3Reducer,promiseDataReducer,pender: penderReducer,});//all() 함수를 이용하여 여러개의 Saga를 합칩니다.//합쳐진 Saga를 rootSaga라고 부릅니다.export function* rootSaga() {yield all([counterSaga()]);}export default rootReducer;
App.js 파일 작성
sagaMiddleware를 생성합니다.
그리고 createStore() 함수의 두번째 파라미터에 sagaMiddleware를 추가해줍니다.
그 다음 sagaMiddleware를 실행해 줍니다.
javascriptimport { createStore, applyMiddleware } from 'redux';import { Provider } from 'react-redux'import rootReducer, {rootSaga} from './src/redux/modules/rootReducer';import penderMiddleware from 'redux-pender';import createSagaMiddleware from 'redux-saga';import { default as HomeScreen } from "./src/screen/HomeScreen";import { default as ReduxSagaScreen } from "./src/screen/redux/ReduxSagaScreen";...생략enableScreens();const Stack = createStackNavigator();function App() {//SagaMiddleware를 생성합니다.const sagaMiddleware = createSagaMiddleware();//creactStore() 함수를 이용하여 Store를 생성합니다.//rootReducer를 첫번째 파라미터로 전달하며, Middleware를 두번째 파라미터로 전달합니다.const store = createStore(rootReducer,applyMiddleware(penderMiddleware(), sagaMiddleware),);//rootSaga를 실행해줍니다.sagaMiddleware.run(rootSaga);//Provider 컴포넌트는 컴포넌트들이 Redux의 Store에 접근 가능하도록 해주는 컴포넌트입니다.//컴포넌트의 Root 위치에 Provider 컴포넌트로 감싸줍니다.return (<Provider store={store}><NavigationContainer><Stack.Navigator initialRouteName="HomeScreen"><Stack.Screen name="ReduxSagaScreen" component={ReduxSagaScreen} />...생략</Stack.Navigator></NavigationContainer></Provider>);}export default App;
ReduxSagaScreen.js 파일 작성
+ 버튼을 클릭하면 INCREASE3_ASYNC_ACTION 액션을 디스패치합니다.
- 버튼을 클릭하면 DECREASE3_ASYNC_ACTION 액션을 디스패치합니다.
javascriptimport {useDispatch, useSelector} from 'react-redux';import {INCREASE3_ASYNC_ACTION,DECREASE3_ASYNC_ACTION,} from '../../redux/modules/counter3';...생략const ReduxSagaScreen = () => {//useSelector는 Store의 State를 조회하는 Hook입니다.const {number} = useSelector(state => ({number: state.counter3Reducer.number,}));//useDispatch는 Store의 함수를 사용 할 수 있게 해주는 Hook 입니다.//dispatch(action) 함수는 State를 변화시키기 위해 Action을 발생시킵니다.const dispatch = useDispatch();const onIncrease = () => {console.log('2. dispatch(INCREASE3_ASYNC_ACTION()) 함수 호출');dispatch(INCREASE3_ASYNC_ACTION());};const onDecrease = () => {console.log('2. dispatch(DECREASE3_ASYNC_ACTION()) 함수 호출');dispatch(DECREASE3_ASYNC_ACTION());};console.log('4. UI 업데이트');return (<View style={styles.screen}><Text style={styles.text}>숫자 : {number}</Text><Button onPress={onIncrease} title="+" /><Button onPress={onDecrease} title="-" /></View>);};...생략export default ReduxSagaScreen;
로그 확인
+ 버튼을 연속 2번 클릭할 때, number가 0->2로 +2 증가 (takeEvery : 모든 액션 처리)
- 버튼을 연속 2번 클릭할 때, number가 2->1로 -1 감소 (takeLatest : 가장 마지막 액션만 처리)