redux-saga 동작 분석
// saga.js
import { put, takeEvery, all } from 'redux-saga/effects'
const delay = (ms) => new Promise(res => setTimeout(res, ms))
export function* helloSaga() {
console.log('Hello Sagas!')
}
// ...
// Our worker Saga: will perform the async increment task
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
// main.js
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import Counter from './Counter'
import reducer from './reducers'
//// default export 인 sagaMiddlewareFactory 를 createSagaMiddleware 에 assign 한 것
import createSagaMiddleware from 'redux-saga'
// import { rootSaga } from './sagas'
import rootSaga from './sagas'
// const store = createStore(reducer)
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
const action = type => store.dispatch({type})
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
render()
store.subscribe(render)
createSagaMiddleware() --> sagaMiddlewareFactory() 가 호출된다. sagaMiddlewareFactory 를 확인해보면 대체로 이전버전의 사용법에 대한 error 를 던져주기 위한 용도인듯 하다. 그외에는
sagaMiddleware 라는 function 을 만들어서 return 해준다.
sagaMiddleware() 를 호출할 때 runSaga 에 첫번째 argument 를 assign 해준다.(이것이
boundRunSaga) 그리고 sagaMiddleware.run 을 하면, 이 boundRunSaga 를 호출한다.
applyMiddleware() 는 단순하다. 기존의 createStore() 를 실행하고, 그 이외에 다른 동작을 하기 위한 함수라고 보면 된다.
python 의 decorator 같은 역할이다.
물론 로직은 좀 더 복잡하게 짜여있다. applyMiddleware() 를 직접실행시키는 것이 아니라createStore 내에서 applyMiddleware() 가 decorator 처럼 동작해야 해서 createStore 내에서 다시 호출(call)한다. 그런 이유로 applyMiddleware() 가 function 을 return 한다. 말이 복잡하다. source code 를 확인하자.
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => { // pass the createStore as an argument
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 기존의 dispatch 에 middleware 를 추가해서 dispatch 를 새롭게 만든다.
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
...
export default function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
...
}
sagaMiddleware.run(rootSaga) 를 하면
runSaga() 가 호출된다. runSaga() 에서는 필요한 설정들에 대한 값들을 set 하고(sagaMonitor 같은)
proc() 를 호출한다.(
proc.js)
proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, noop)
proc 에서 stdChannel 를 생성하는데, 이녀석은 약간의 경고문구만 추가한
eventChannel 이다. 이녀석은
createSagaMiddleware 를 할 때 생성된다.
proc 에서 next() 를 호출하고, next 는 iterator.next() 를 호출한다. iterator 를 이용해서
all() 로 묶인 task 들을 한번에 처리한다.
// saga.js
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
...
// runSaga.js
function runSaga(_ref, saga) {
...
var iterator = saga.apply(void 0, args);
...
return immediately(function () {
var task = proc(env, iterator, context, effectId, __chunk_1.getMetaInfo(saga),
/* isRoot */
true, __chunk_1.noop);
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task);
}
return task;
});
}
function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) {
if (iterator[__chunk_1.asyncIteratorSymbol]) {
throw new Error("redux-saga doesn't support async generators, please use only regular ones");
}
...
next(); // then return the task descriptor to the caller
return task;
/**
* This is the generator driver
* It's a recursive async/continuation function which calls itself
* until the generator terminates or throws
* @param {internal commands(TASK_CANCEL | TERMINATE) | any} arg - value, generator will be resumed with.
* @param {boolean} isErr - the flag shows if effect finished with an error
*
* receives either (command | effect result, false) or (any thrown thing, true)
*/
function next(arg, isErr) {
try {
var result;
if (isErr) {
result = iterator.throw(arg); // user handled the error, we can clear bookkept values
clear();
} else if (__chunk_1.shouldCancel(arg)) {
/**
getting TASK_CANCEL automatically cancels the main task
We can get this value here
- By cancelling the parent task manually
- By joining a Cancelled task
**/
...
} else if (__chunk_1.shouldTerminate(arg)) {
// We get TERMINATE flag, i.e. by taking from a channel that ended using `take` (and not `takem` used to trap End of channels)
...
} else {
result = iterator.next(arg);
}
...
} catch (error) {
if (mainTask.status === CANCELLED) {
throw error;
}
mainTask.status = ABORTED;
mainTask.cont(error, true);
}
}
...
}
redux-saga 사용
redux-saga 는 특정 이벤트가 끝날때에 대한 작업을 미리 등록해 놓을 수 있다.(saga.js)
그래서 원래라면,
- 특정 이벤트가 발생하고,
- 그 작업을 수행한 후(event handler)에
- 우리가 원하는 작업을 실행하기 위해서 우리의 code를 추가해야 한다.(callback)
하지만 redux-saga 는 그부분의 대한 구현에 관계없이 구현을 하면 된다.
redux-saga 를 이용해서 event 가 끝나는 시점에 호출되는 대신에,
- 먼저 특정 event 를 기다리는 code 를 실행하고
- event 가 발생할 때 까지 control 을 다른 곳에 넘겨준다. 그리고 event 가 발생하면 다시 control 을 받아온다.(yield)
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
간단히 이야기 하면, await/async 를 사용하게 해주는 것이라 보면 된다.
아래 글을 읽으면 자세한 이야기를 알 수 있다.