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()
createSagaMiddleware() --> sagaMiddlewareFactory() 가 호출된다. sagaMiddlewareFactory 를 확인해보면 대체로 이전버전의 사용법에 대한 error 를 던져주기 위한 용도인듯 하다. 그외에는 sagaMiddleware 라는 function 을 만들어서 return 해준다.sagaMiddleware() 를 호출할 때 runSaga 에 첫번째 argument 를 assign 해준다.(이것이 boundRunSaga) 그리고 sagaMiddleware.run 을 하면, 이 boundRunSaga 를 호출한다.
createStore() / applyMiddleware()
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)
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 를 이용해서 event 가 끝나는 시점에 호출되는 대신에,
- 먼저 특정 event 를 기다리는 code 를 실행하고
- event 가 발생할 때 까지 control 을 다른 곳에 넘겨준다. 그리고 event 가 발생하면 다시 control 을 받아온다.(yield)
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
간단히 이야기 하면, await/async 를 사용하게 해주는 것이라 보면 된다.
아래 글을 읽으면 자세한 이야기를 알 수 있다.
댓글 없음:
댓글 쓰기