redux-saga를 이용한 Counter 만들기

Goal

redux와 redux-saga를 이용하여 간단한 Counter 웹을 만들어 봄으로써 redux-saga에 대한 이해를 높인다.

redux-saga는 무엇이고 언제 사용할까?

redux-saga redux-saga repo

The mental model is that a saga is like a separate thread in your application that’s solely responsible for side effects.

Counter 만들기

reducer 먼저 만들기

// reducer.js
export function reducer(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

redux와 redux-saga를 프로젝트에 셋업하기

yarn add redux redux-saga
// index.js
// ...
import createSagaMiddleware from "redux-saga";
import { applyMiddleware, createStore } from "redux";
import { reducer } from "./reducer";

const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, applyMiddleware(sagaMiddleware))

// applyMiddleware 이후 run()을 해야 한다.
sagaMiddleware.run(rootSaga);

const rootElement = document.getElementById("root");
function render() {
  ReactDOM.render(
    <App />,
    rootElement
  );
}

render();
// store의 상태가 변화할 때마다 render를 호출한다.
store.subscribe(render);

Counter 컴포넌트 작성하기

// Counter.jsx

const Counter = ({value, onIncrementAsync, onDecrementAsync}) => {

    return (
        <div>
            <h1>{value}</h1>
            <button onClick={onIncrementAsync}>INCREMENT ASYNC</button>
            <button onClick={onDecrementAsync}>DECREMENT ASYNC</button>
        </div>
    )
}

export default Counter

Saga 작성하기

You can view Effects like instructions to the middleware to perform some operation (e.g., invoke some asynchronous function, dispatch an action to the store, etc.).

// sagas.js
import { put, takeEvery, all } from "redux-saga/effects";

const delay = (ms) => new Promise(res => setTimeout(res, ms))

function* incrementAsync() {
    // 미들웨어에 Promise를 전달하면, 미들웨어는 프로미스가 resolve될 때까지 기다린다.
    yield delay(1000)
    // put(action): action을 store에 디스패치하는 effect를 미들웨어에 전달한다.
    yield put({type: "INCREMENT"})
}
// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
// INCREMENT_ASYNC이 디스패치되었을 때, incrementAsync을 'spawn'한다.
function* watchIncrementAsync() {
  yield takeEvery("INCREMENT_ASYNC", incrementAsync);
}
export default function* rootSaga() {

    // Creates an Effect description that instructs the middleware to run multiple Effects 
    // in parallel and wait for all of them to complete.
  yield all([
      // 제너레이터 함수를 호출하여 effect를 등록한다.
    watchIncrementAsync()
  ])
}
// index.js
// ...
import rootSaga from './sagas'
sagaMiddleware.run(rootSaga);

디스패치 함수를 작성하여 Props로 전달한다

// index.js
const incrementAsync = () => {
  store.dispatch({
    type: "INCREMENT_ASYNC"
  })
}

function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrementAsync={incrementAsync}
    />,
    rootElement
  );
}

render();
store.subscribe(render);

완성된 코드