Redux-Saga

Redux-Saga란 무엇인가?


Redux-saga란, side effect를 관리하기 위한 middleware이다.

Side effect란, 애플리케이션의 부작용들(데이터 요청 등의 비동기 작업/브라우저 캐시 같은 순수하지 않은 것들)을 말한다.

middleware란, 액션이 dispatch 되어서 리듀서에서 이를 처리하기 전에 그 사이에서 중간자로써 지정된 특정 작업을 하는 것을 말한다.


Redux-Saga를 왜 사용하는가?


비동기 처리 등 순수하게 액션만을 전달하는 것 이상으로 발생하는 상황들(side effects)을 다룰 때, 이 미들웨어를 사용한다.


Redux-Thunk 대신에 Redux-Saga를 사용하는 이유는 무엇인가?


redux-thunk를 사용하면, 비동기 처리를 Action Creator(액션 생성자 함수)에서 작성하게 되어, 로직이 복잡해진다. 기본적으로 Redux에서 Action Creator에서는 액션을 객체형태로 생성하여 dispatch 하는데, redux-thunk에서는 액션을 함수형태로 생성하여 dispatch 하여, Action Creator에 비동기처리 코드나 관련된 로직이 들어가게 된다.

그런데, Redux-saga는 비동기 처리를 기술하는 전용의 방식인 task로 쓰여있다. 즉, 비동기 처리를 각 saga(task)에서 명령을 내리고, 동작을 미들웨어에서 처리한다. 그래서, redux에서의 action creator에서 기존과 그대로 액션을 객체형태로 생성하여 dispatch 할 수가 있고, saga에서의 코드도 더 간단해진다. 결국, Action Creator는 본래의 모습을 되찾아, action 객체를 생성하여 돌려주는 순수한 상태로 돌아가게 된다.

결론적으로, Redux-Saga를 사용하게 되면, 비동기 처리를 액션 생성자로부터 분리하여 작성하기 때문에, 가독성도 좋고 콜백 처리 등에 더 수월하다.


Redux-Saga Architecture


Redux-Saga Architecture
출처: Redux-Saga Architecture


Redux-Saga Flow


Redux-Saga Architecture
출처: Redux-Saga Architecture)

위 Flow를 정리하면 다음과 같다.

  1. 어떤 비동기 액션이 dispatch 되면, Reducer에 먼저 도달한다. 정확하게는 액션이 Reducer로 지나가는 것을 본 후, Redux-Saga에 액션을 처리한다. Reducer는 순수 함수라는 규칙이 있기 때문에, 비동기 액션을 처리하지 못한다.
  1. 이 비동기 액션을 Redux-Saga에 있는 watcher saga가 보고 있다가, watcher saga에 등록되어 있는 task를 수행한다. 이 watcher saga의 역할은 어떤 비동기 액션이 dispatch 되면, 어떤 task를 수행하도록 등록하는 것이다. 이때, takeEvery라는 헬퍼 이펙트를 사용하는데, 이 이펙트는 여러개의 task를 동시에 시작할 수 있다. 즉, 1개 혹은 아직 종료되지 않은 task가 있더라도 새로운 task를 시작할 수 있다.
1
2
3
4
// INCREMENT_ASYNC 액션이 Dispatch 되면 `incrementAsync`를 수행하도록 등록한다.
export function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync)
}
  1. incrementAsync를 수행하는데, 만약 task가 2초 마다 +1씩 증가하는 함수라고 한다면, 다음과 같이 작성한다.
1
2
3
4
function* incrementAsync(action) {
yield delay(2000) // 2초를 기다리고
yield put({ type: INCREMENT }) // INCREMENT 액션을 Dispatch한다.
}
  1. 위 task가 수행될 때, 먼저 첫번째 줄에 있는 delay(2000)이 yield 된다. delay는 설정된 시간 이후에 resolve를 하는 Promise 객체를 리턴하는 함수이다. 이 Promise가 미들웨어에 yield 될때, 미들웨어는 Promise가 끝날때까지 Saga를 일시정지 시킨다.(generator 함수의 특징) 즉, 이 부분은 동기적으로 동작한다.
  1. 2초후, Promise가 한번 resolve 되면, 미들웨어는 saga(task)를 다시 작동시키면서, 다음 yield까지 코드를 실행한다. 이런 방식으로 saga(task)에서 1개씩 yield되고, 그 yield 된 것이 미들웨어에 의해 동작이 완료되면, 그때 그 다음줄에 있는 객체가 yield된다.
  1. 어떤 객체를 yield할때, 앞에 오는 것들(put, call 등)을 이펙트라고 한다. 이펙트란, 미들웨어에 의해 수행되는 명령을 담고있는 간단한 자바스크립트 객체를 말한다. 미들웨어가 saga에 의해 yield 된 이펙트를 받을때, saga는 이펙트가 수행될때까지 정지되어 있다.
  1. saga에서 put을 통해 객체를 dispatch하면, 이 객체는 reducer로 가게된다.
  1. 두 saga를 모두 한번에 실행하게 해주기 위해, rootSaga를 이용해 사용한다.
1
2
3
4
5
6
7
// 모든 Saga들을 한번에 시작하기 위한 단일 entry point 이다.
export default function* rootSaga() {
yield all([
incrementAsync(),
watchIncrementAsync()
])
}


Redux-Saga 사용방법


Redux-Saga를 사용하는 방법은 다음과 같다.

  • saga 미들웨어 작성(sagas.js)
    • 비동기 처리를 위한 task 작성(제너레이터 함수) - worker saga
    • 각각의 어떤 비동기 액션을 처리하기 위해 watch 함수를 작성하고, 그 안에 takeEvery 이펙트 펠어 함수를 사용 - watcher saga
    • 모든 saga들을 한번에 실행하기 entry point 작성 - rootSaga 작성
      • rootSaga에 들어있는 두 saga가 호출된 결과의 배열을 yield 한다. 이것은 생성된 두 제너레이터가 병렬로 시작된다는 것을 의미.
  • Saga 미들웨어를 Redux 스토어에 연결(main.js)
    • applyMiddleware
    • sagaMiddleware.run 등 작성
  • 비동기 호출을 위한 이벤트 등록(Counter.js)

saga 미들웨어 작성(sagas.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// sagas.js

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

// watcher saga
export function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync)
}

// worker saga
function* incrementAsync(action) {
yield delay(2000)
yield put({ type: INCREMENT })
}

// rootSaga
export default function* rootSaga() {
yield all([
incrementAsync(),
watchIncrementAsync()
])
}

Saga 미들웨어를 Redux 스토어에 연결(main.js)

1
2
3
4
5
6
7
8
9
// main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)

비동기 호출을 위한 이벤트 등록(Counter.js)

1
2
3
4
5
6
7
8
9
10
11
// Counter.js
const Counter = ({ value, onIncrementAsync }) =>
<div>
<button onClick={onIncrementAsync}>
Increment after 2 second
</button>
<hr />
<div>
Counter: {value} times
</div>
</div>

비동기 호출을 처리하기 위해 Redux와 연결해주는 컨테이너 컴포넌트(CounterContainer.js)

1
2
3
4
5
6
7
8
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>
)
}