Promise를 사용하면 resolve 시점에서 객체를 직접 리턴할 수 없다. 그래서 비동기 처리를 할 때 액션 객체를 반환하는 대신 dispatch를 인자로 하는 함수를 리턴해 이 함수안에서 데이터펫칭을 비롯한 네트워킹, 다수의 디스패치 등을 할 수 있게 해주는 미들웨어이다.
export function fetchCars() {
// dispatch를 인자로 하는 함수를 리턴 한다.
return dispatch => {
// 요청이 시작됨을 알린다.
dispatch({ type: FETCH_CARS_BEGIN });
// API 요청을 실행하며 완료 시 함수는 종결 된다
return axios
.get(`${API}/cars`)
// 성공 시 성공했음을 알리고 받아온 자료를 payload 에 담아 리듀서로 보낸다
.then(res =>
dispatch({ type: FETCH_CARS_SUCCESS, payload: res.data })
)
// 실패 시 실패했음을 알리고 받은 에러를 payload 에 담아 리듀서로 보낸다
.catch(err =>
dispatch({ type: FETCH_CARS_FAILURE, payload: err }));
};
}
클로저 패턴을 사용해서 소스코드가 깔끔하지 못하다.
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// saga 미들웨어를 생성합니다.
const sagaMiddleware = createSagaMiddleware()
// 스토어에 mount 합니다.
const store = createStore(
reducer,
// redux의 미들웨어로 sagaMiddleware를 사용합니다.
applyMiddleware(sagaMiddleware)
)
// 그리고 saga를 실행합니다.
sagaMiddleware.run(mySaga)
// 애플리케이션을 render합니다.
// action
export const FETCH_CARS = 'FETCH_CARS'
export const SAVE_FETCHED_CARS = 'SAVE_FETCHED_CARS'
export const fetchCars = () => ({
type: FETCH_CARS
})
export const saveFetchedCars = cars => ({
type: SAVE_FETCHED_CARS,
payload: cars
})
// reducer
const INITIAL_STATE = {
cats: []
}
export default (state = INITIAL_STATE, {type, payload}) => {
switch (type) {
case SAVE_FETCHED_CARS:
return {
...state,
cars:payload
}
default:
return state
}
}
// saga
function* fatchCarsSaga() {
// try-catch 구문으로 오류 제어
try {
const { data } = yield axios.get('/cars')
yield put(actions.saveFetchedCars(data))
// put : redux store에 dispatch하는 역할
} catch (error) {
yield put(actions.에러처리 액션)
}
}
// 아래의 함수는 추후 sagaMiddleware.run(watchSaga)에 사용되어
// 언제나 액션들을 리스닝한다.
function* watchSaga() {
// type의 action이 실행되면 fetchCarSaga가 항상(every) 실행
yield takeEvery(FETCH_CARS, fetchCarsSaga)
}
만약 여러개의 saga를 묶어서 사용하고 싶다면 all Effect를 사용해서 아래와 같이 만들어줄 수 있다.
import { all } from 'redux-saga/effects';
// all 함수를 통해 Saga들을 하나로 묶어줄수 있다.
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
// 그후 아래와 같이 sagaMiddleware.run()에 넣어주면 된다.
sagaMiddleware.run(rootSaga)
redux-thunk
redux-saga의 기본
redux-saga에 대하여
redux-saga 소개
Redux-saga의 Saga가 바로 제네레이터함수
라는 사실..!
function*
키워드로 작성하는 함수// 제네레이터함수
function* myGeneratorFunction() {
yield 1;
yield 2;
yield 3;
}
// 제네레이터
const generator = myGeneratorFunction();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
/* 제너레이터는 이터레이터 프로토콜을 따른다. */
// 1. "function"
typeof generator.next;
// 2. {done: boolean, value: any} 반환
generator.next();
//-----//
/* 제너레이터는 이터러블 프로토콜을 따른다. */
// 1. "function"
typeof generator[Symbol.iterator];
// 2. 이터레이터가 반환된다.
const iterator = generator[Symbol.iterator]();
typeof iterator.next(); // {done: boolean, value: any}
// 제너레이터는 이터러블이면서 이터레이터라는 것인데,
// 이터러블에서 반환하는 이터레이터가 바로 자기 자신이다.
Redux-Saga에서 말하는 Saga는 바로 제너레이터함수다. 그럼 왜 Saga를 제너레이터함수로 구현할까? 이는 곧 Redux-Saga가 이펙트라 부르는 것들을 어떻게 만들고 사용하는지와 연관된다. 우리가 Redux-Saga를 사용한다는 것은 곧 Redux-Saga 미들웨어에 우리의 Saga를 등록하고 수행시킨다는 뜻이다. 미들웨어는 Saga를 끊임없이 동작시킨다.
// Saga의 초기화, 시작 코드에는 항상 "run"이 있다.
middleware.run(RootSaga);
Saga는 제너레이터함수이고, 미들웨어는 Saga에게 yield 값을 받아서 또 다른 어떤 동작을 수행할 수 있다. Saga는 명령을 내리는 역할만 하고, 실제 어떤 직접적인 동작은 미들웨어가 처리할 수 있다는 뜻이다. redux-thunk와의 가장 큰 차이점이다.
// redux-thunk 비동기 함수
function asyncIncrement() {
return async (dispatch) => {
await delay(1000);
dispatch({type: 'INCREMENT'});
};
}
위 함수는 스스로 비동기적인 처리를 직접 수행한다. 저 함수에 대한 테스트가 필요하다면, 1초를 기다리고 dispatch 하는 것을 어떻게 증명할지 생각해보자. 딱히 마음에 드는 방법은 떠오르지 않는다. 문제는 함수 내부에 비동기적인 로직이 그대로 녹아있다는 것이다.
// redux-saga
function* asyncIncrement() {
// Saga는 아래와 같이 간단한 형태의 명령만 yield 한다.
yield call(delay, 1000); // {CALL: {fn: delay, args: [1000]}}
yield put({type: 'INCREMENT'}); // {PUT: {type: 'INCREMENT'}}
}
call이든 put이든 모두 직접적인 처리를 하지 않는다(call, put은 이펙트 생성자(Effect creator)라 부른다). 명령을 만들어주기만 하고, 이 명령에 따른 직접적인 처리는 모두 미들웨어가 한다. 그래서 이런 Saga는 테스트도 정말 간단하다.
// TestCase
// 실제로 Delay 시키는게 아니라 이에 대한 명령뿐이므로 테스트에서 1초씩 기다릴 필요가 없다.
// 단지 어떤 명령이 내려지는지만 확인하면 된다.
const gen = asyncIncrement();
expect(gen.next().value).toEqual(call(delay, 1000));
expect(gen.next().value).toEqual(put({type: 'INCREMENT'}));
그리고 Saga에서 비동기 처리가 아무리 복잡해도 대부분은 if
, else
, for
와 같은 간단한 코드만으로 구현할 수 있다. 스코프가 복잡해지는 것도 아니다. Redux-Saga는 이런 이점을 위해 제너레이터함수를 Saga로 사용한다.
call
이나 put
모두 이펙트 생성자고, 생성된 이펙트는 모두 일반 자바스크립트 객체일 뿐이다.Q) Saga는 반드시 이펙트만을 yield해야 하는 것인가?
A) 아니다. 일반적인 Promise도 yield 할 수 있고, 미들웨어는 이 역시도 훌륭히 resolve나 reject를 기다려줄 것이다. 하지만 이런 비동기 로직을 Saga 내부에서 직접 처리하면 테스트, 여러 다른 이펙트들과의 상호작용이 어렵다. thunk에서 크게 달라지는 점이 없다. 때문에 되도록 이펙트만을 yield 하는 Saga를 작성하길 추천한다.
Saga의 이펙트는 10가지 이상으로 우리가 활용하는 데 큰 문제가 없을 만큼 다양하다. 비록 이 글에서 이펙트들을 하나하나 설명하진 못했지만, Effect creators API를 참고한다면 Saga를 적극적으로 활용하는 데 많은 도움이 될 것이다. 이펙트는 단순히 테스트만을 위해서 만든 것이 아니다. 이런 이펙트들은 pulling이나, non-blocking, blocking, parallel 등의 다양한 특징들을 가지고 있고, 이런 특징들을 이용해서 정말 수많은 동작들을 손쉽게 처리할 수 있다. 공식 문서의 Advanced Concepts에 그 내용들이 잘 나타나 있다.
Redux-Saga: 제너레이터와 이펙트
generator functions & Redux Saga
mdn yield