Redux 구조에서 사이드 이펙트 작업시(비동기 요청 등) 사용할 라이브러리 중 redux-thunk, redux-saga, redux-observable 확인하고 각각의 장/단점을 살펴보겠습니다.
본론에 앞서 기본적인 Redux의 data flow 부터 확인해보겠습니다.
Redux data flow
import { createStore } from 'redux';
// Action 타입
const TYPE = {
INCREMENT_COUNTER: 'INCREMENT_COUNTER'
};
// Action 객체
const incrementCounterAction = {
type: TYPE.INCREMENT_COUNTER,
payload: {
count: 1
}
};
// Action 생성자
function increment() {
return incrementCounterAction;
};
// 초기 Store
const initialState = {
count: 0
};
// Reducer
function reducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case TYPE.INCREMENT_COUNTER:
state.count += payload.count
default:
return state;
}
};
// Store 생성
const store = createStore(reducer);
// dispatch 수행
store.dispatch(increment());
"Action"이라고 부르는 오브젝트를 "Dispatch"하여 값을 전달하면 "Reducer"에서 "Store"의 값을 적용하는것이 기본적인 flow 입니다.
아래의 이미지는 Redux 공식 사이트에서 제공되고 있는 이미지입니다.
앞으로 살펴볼 라이브러리들은 미들웨어(Middleware)에서 동작합니다.
실제로는 dispatch된 action들은 무조건 모두 reducer로 전달됩니다.
하지만, 미들웨어를 설정하면 그 action들을 미들웨어에서도 받을 수 있도록 중간에 레이어를 두었다고 생각하시며 됩니다.
세가지 라이브러리를 동일한 예제를 기반으로 설정법과 기본적인 사용 방법을 살펴보겠습니다.
동일하게 사용되는 action, reducer, store는 아래와 같습니다.
// Action 타입
const TYPE = {
USER_FETCH_REQUESTED: 'USER_FETCH_REQUESTED',
USER_FETCH_SUCCEEDED: 'USER_FETCH_SUCCEEDED',
USER_FETCH_FAILED: 'USER_FETCH_FAILED'
}
// 초기 Store
const initialState = {
user: {},
error: {}
}
// Reducer
function reducer(state = initialState, action){
const { type, payload } = action;
switch (type) {
case TYPE.USER_FETCH_SUCCEEDED:
state.user = payload.user;
case TYPE.USER_FETCH_FAILED:
state.error = payload.error;
default:
return state;
}
}
// Action 객체
const incrementCounterAction = {
type: TYPE.INCREMENT_COUNTER,
payload: {
count: 1
}
}
// Action 생성자
function fetchRequest(userId) {
return {
type: TYPE.USER_FETCH_REQUESTED
payload: {
userId
}
};
}
function fetchSucceeded(user) {
return {
type: TYPE.USER_FETCH_SUCCEEDED
payload: {
user
}
};
}
function fetchFailed(error) {
return {
type: TYPE.USER_FETCH_FAILED
payload: {
error
}
};
}
Redux-thunk (github.com/reduxjs/redux-thunk)
redux-thunk는 Redux를 처음 접할경우 가장 먼저 접하게 되는 라이브러리 입니다.
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import Api from '...';
import reducer from '../reducer';
import { fetchSucceeded, fetchFailed } from 'action';
// Store 생성(thunk 추가)
const store = createStore(reducer, applyMiddleware(thunk));
function fetchUserAsync() {
return async (dispatch, getState) => {
try {
const res = await Api.fetchUser(123);
dispatch(fetchSucceeded(res.user);
} catch(error) {
dispatch(fetchFailed(error) )
}
}
}
// dispatch 수행
store.dispatch(incrementAsync());
Redux data flow에 따라 dispatch를 통해 전달되는것은 액션 오브젝트인데, redux-thunk에서는 액션이 아닌 함수도 전달이 가능하도록 하고 있습니다.
그 함수내에서 사이드 이펙트 동작을 수행하고, 다시 dispatch를 해줄 수 있습니다.
사용법이 매우 간단합니다.
그러나 dispatch에 전달할 수 있는 것이 액션 오브젝트뿐만 아니라, 함수도 전달이 되는 구조는 애플리케이션이 좀 더 다수의 액션이 요구되어지는 상황에서는 관리가 어려워 질 수 있습니다. 자유도가 높은 만큼 그에 맞는 구조가 필요합니다.
Redux-saga (redux-saga.js.org/)
redux-saga는 javascript의 제너레이터 함수를 활용한 방식입니다. 기본적인 구조는 watcher-worker로 이루어져 있습니다.
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects'
import Api from '...'
import reducer from '../reducer';
import { fetchRequest, fetchSucceeded, fetchFailed } from 'action';
// Saga 미들웨어 생성
const sagaMiddleware = createSagaMiddleware();
// Store 생성 (Saga 미들웨어 추가)
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
// Saga 미들웨어 실행
sagaMiddleware.run(fetchRequestSaga);
// watcher Saga
function* fetchRequestSaga() {
yield takeEvery(TYPE.USER_FETCH_REQUESTED, fetchUser);
}
// worker Saga
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put(fetchSucceeded(user));
} catch (error) {
yield put(fetchFailed(error));
}
}
// dispatch
store.dispatch(fetchRequest(123));
store 생성후에 watcher 역할을 하는 saga가 실행될수 있도록 .run() 을 실행합니다. 그러면 .run() 에 주입한 saga 함수가 동작합니다.
(saga에서는 한개의 task를 saga라고 부릅니다.)
그럼 watcher 역할을 하는 saga에서 `takeEvery`라는 헬퍼를 통해 dispatch된 액션을 대기하고 매칭되는 액션이 발생한 경우 worker saga를 실행시킵니다.
worker saga에서 사이드 이펙트 작업을 완료후 put이라는 헬퍼를 통해 다시 action을 dispatch하면 리듀서가 해당되는 액션을 받아 store에 값을 추가합니다.
action에 대해 반응할수 있으며, Redux data flow에 맞게 액션 오브젝트만 전달하기 때문에 관리 측면에 복잡도가 줄어듭니다. 그러나 사용전에 제너레이터 함수에 대한 이해와 saga에서 제공되는 헬퍼들을 숙지해야하는 부분이 있습니다.
Redux-observable (redux-observable.js.org/)
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import Api from '...';
import reducer from '../reducer';
import { fetchRequest, fetchSucceeded, fetchFailed } from 'action';
// observable 미들웨어 생성
const epicMiddleware = createEpicMiddleware();
// Store 생성 (observable 미들웨어 추가)
const store = createStore(reducer, applyMiddleware(epicMiddleware));
// observable 미들웨어 실행
sagaMiddleware.run(fetchRequestEpic);
function fetchRequestEpic(action$, _state$) {
action$.pipe(
ofType(TYPE.USER_FETCH_REQUESTED),
mergeMap(action =>
from(api.fetchUser(action.payload.userId)).pipe(
map(res => fetchSucceeded(res.user)),
catchError(err => of(fetchFailed(errror)));
)
)
)
};
// dispatch
store.dispatch(fetchRequest(123));
Redux-observable도 saga와 동일하게 store생성 후 .run()을 통해 실행시키는 인터페이스는 동일합니다. .run() 에 주입한 epic 함수가 동작합니다.
(observable에서는 한개의 task를 epic이라고 부릅니다.)
액션이 발생하면 ofType이라는 헬퍼를 사용해 원하는 타입을 매칭 하고, 매칭되는 액션이 전달되었을 경우 뒤이어 작업할 테스크들을 나열합니다. 그리고 원하는 액션을 실행시켜서 store에 값을 추가합니다.
saga와 마찬가지로 액션 오브젝트를 전달하기 때문에 복잡도가 올라가지 않지만, 사용전에 RxJS에 대한 이해도와 RxJS 문법 및 observable에서 제공되는 헬퍼들을 숙지해야하는 부분이 있습니다.
결론
종류 | 장점 | 단점 |
Redux-thunk |
러닝커브가 낮다. |
액션을 통해 함수 전달이 가능한 thunk의 특성이 코드에 혼란을 줄 수 있다. |
Redux-saga |
제공되는 Helper 들이 많다. |
러닝커브가 높다. (then thunk) |
Redux-observable |
제공되는 Helper 들이 많다. |
러닝커브가 높다. (then saga) |
Redux-thunk, Redux-saga, Redux-observable의 각각의 특성을 아주 간단히 요약해보았습니다.
해당 라이브러리를 사용하시기 전에 참고하시면 좋을것 같습니다.
참고
'개발' 카테고리의 다른 글
Exponential Backoff (0) | 2022.12.22 |
---|---|
Redis 에서 "Used Memory RSS" 가 지속적으로 증가할때 (0) | 2022.12.22 |
Android WebView Debugging (0) | 2021.07.23 |
Cookie recipes - SameSite and beyond (0) | 2020.08.29 |
프로그레시브 이미지 렌더링 (Progressive Image Rendering) (0) | 2018.12.24 |