React에서 Redux를 사용하는 방법은 여러가지가 존재한다. 그중 필자는 Redux Saga를 사용한다. 물론 Redux Saga를 중점으로 공부했기 때문이다. 리액트를 다루는 기술에서 나온 방법을 추로 사용하고 있다.
우선 Redux Saga를 왜 사용해야하는 것일까?
Redux Saga 사용하는 이유
Redux Saga는 다음과 같은 상황에서 유리하게 작용된다.
- 불필요한 중복요청 방지
- 특정 액선이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
- 웹 소켓을 사용할 때
- API 요청 실패 시 재요청해야 할 때
Redux Saga를 사용하기 위해서는 ES6의 제네레이터(Generator) 함수라는 문법을 알고 있어야 한다. 일반적인 상황에서는 사용되지 않음으로 이해하기 너무 어렵다. 필자도 마찬가지로 이해하기가 너무 힘들다.
제네레이터 함수에 대해서는 Generator Function(제네레이터 함수)란 무엇인가?에서 정리되어 있다.
Redux Saga 사용법
개인 프로젝트에서 사용했던 방법으로 설명하겠다. 우선 이 프로젝트는 비트코인의 정보들을 실시간으로 가져오는 프로젝트이기 때문에 API 요청을 해야한다.
// ./lib/apis/client.js | |
import axios from 'axios'; | |
const client = axios.create(); | |
export default client; |
axios에서 추가로 header나 무엇인가를 넣어야한다면 export default client위에 client.default.~~~ 로 작업하면 된다.
그 다음 비트코인 거래소인 빗썸의 API를 불러올 코드를 작성한다.
// ./lib/apis/bitcoins.js | |
import client from './client'; | |
export const getChart = () => ( | |
client.get('https://api.bithumb.com/public/ticker/ALL') | |
); |
이제 Redux와 Redux Saga를 가지고 Actions를 작성한다
// ./modules/bitCoinCharts.js | |
import { createAction, handleActions } from 'redux-actions'; | |
import createRequestSaga, { | |
createRequestActionTypes | |
} from '../lib/creasteRequestSaga'; | |
import * as bitCoinsAPI from '../lib/apis/getBitcoin'; | |
import { takeLatest } from 'redux-saga/effects'; | |
const [ | |
BIT_COINS_CHART, | |
BIT_COINS_CHART_SUCCESS, | |
BIT_COINS_CHART_FAILURE | |
] = createRequestActionTypes('bitCoins/BIT_COINS_CHART'); | |
export const bitCoinsChart = createAction(BIT_COINS_CHART); | |
const bitCoinsChartSaga = createRequestSaga(BIT_COINS_CHART, bitCoinsAPI.getChart); | |
export function* bitCoinsSaga() { | |
yield takeLatest(BIT_COINS_CHART, bitCoinsChartSaga); | |
} | |
const initialState = { | |
status: null, | |
datas: null, | |
error: null, | |
} | |
const bitCoins = handleActions( | |
{ | |
[BIT_COINS_CHART_SUCCESS]: (state, { payload: res }) => ({ | |
...state, | |
status: res.status, | |
datas: res.data | |
}), | |
[BIT_COINS_CHART_FAILURE]: (state, { payload: error }) => ({ | |
...state, | |
error | |
}) | |
}, | |
initialState | |
) | |
export default bitCoins; |
추가로 Actions에 대해 캡슐화와 API 호출 / 종료에 대한 로직을 확인하기 위해 아래의 2개의 파일을 추가로 작업했다.
// ./lib/createRequestSaga.js | |
import { call, put } from 'redux-saga/effects'; | |
import { startLoading, finishLoading } from '../modules/loading'; | |
export const createRequestActionTypes = type => { | |
const SUCCESS = `${type}_SUCCESS`; | |
const FAILURE = `${type}_FAILURE`; | |
return [type, SUCCESS, FAILURE]; | |
} | |
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)); | |
} | |
} |
// ./modules/loading.js | |
import { createAction, handleActions } from 'redux-actions'; | |
const START_LOADING = 'loading/START_LOADING'; | |
const FINISH_LOADING = 'loaidng/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; |
그 다음 이 Redux들을 통합으로 관리할 RootReducer를 작성한다
// ./modules/index.js | |
import { combineReducers } from 'redux'; | |
import { all } from 'redux-saga/effects'; | |
import bitCoinCharts, { bitCoinsSaga } from './bitCoinCharts'; | |
import loading from './loading'; | |
const rootReducer = combineReducers({ | |
bitCoinCharts, | |
loading | |
}); | |
export function* rootSaga() { | |
yield all([bitCoinsSaga()]); | |
} | |
export default rootReducer; |
7번째 줄의 takeLatest가 Redux Saga를 이용하는 첫번째 이유의 역할을 한다.
takeLatest는 가장 마지막에 호출된 요청만 처리한다. 그 외에도 takeEvery도 있는데 이건 여러번의 호출 전부다 처리한다. 처리량이 많으면 안될 경우 takeLatest를 사용하길 바란다.
마지막으로 Redux는 Store라는 것을 통해 state를 관리한다. 그에 관련된 코드는 App.js에서 작업한다.
import React, { useEffect } from 'react' | |
import SplashScreen from 'react-native-splash-screen'; | |
import { Provider } from 'react-redux'; | |
import { createStore, applyMiddleware } from 'redux'; | |
import rootReducer, { rootSaga } from './modules'; | |
import createSagaMiddleware from 'redux-saga'; | |
import { composeWithDevTools } from 'redux-devtools-extension'; | |
import MainScreen from './components/main/MainScreen'; | |
const sagaMiddleware = createSagaMiddleware(); | |
const store = createStore( | |
rootReducer, | |
composeWithDevTools(applyMiddleware(sagaMiddleware)) | |
); | |
sagaMiddleware.run(rootSaga); | |
const App = () => { | |
useEffect(() => { | |
setTimeout(() => { SplashScreen.hide() }, 1000); | |
}, []) | |
return ( | |
<Provider store={store}> | |
<MainScreen /> | |
</Provider> | |
) | |
} | |
export default App |
API를 호출하고 state 값을 가지고 올때는 dispatch를 통해 호출하고 useSelector를 통해 state 값을 가지고 온다.
// ./components/main/MainScreen.js | |
import React, { useEffect, useRef } from 'react' | |
import { Container, Header, Title, Body, Text, View, Spinner, Icon, Content, Card, Right, CardItem } from 'native-base'; | |
import { Dimensions, StyleSheet, Alert } from 'react-native'; | |
import { useDispatch, useSelector } from 'react-redux'; | |
import { bitCoinsChart } from '../../modules/bitCoinCharts'; | |
import moment from 'moment'; | |
import ChartItem from '../chart/ChartItem'; | |
const MainScreen = () => { | |
const dispatch = useDispatch(); | |
const { status, charts, date, error, loading } = useSelector(state => { | |
...생략 | |
}) | |
useEffect(() => { | |
dispatch(bitCoinsChart()); | |
}, [dispatch]) | |
... 생략 | |
} | |
export default MainScreen |