Front-End

vsoghlv@naver.com

React.js redux Saga 사용해보기

비동기 카운터 구현

이전에 thunk 를 공부하기 위해 만들었던 비동기 카운터를 리덕스 사가로 구현해 보자. 먼저 설치하자

yarn add redux-saga

아래는 기존의 카운터 모듈이다.

//modules/counter.js
//액션타입
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

//액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

//thunk 함수(dispatch, getState 중 필요한 것만 가져와도 된다.)
export const increaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(increase());
  }, 1000);
};
export const decreaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(decrease());
  }, 1000);
};

//초기 상태
const initialState = 0;

//리듀서
export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}

먼저 기존의 thunk 함수를 아래와 같이 바꿔준다.

import { delay, put } from "redux-saga/effects";

//액션타입
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
const INCREASE_ASYNC = "counter/INCREASE_ASYNC";
const DECREASE_ASYNC = "counter/DECREASE_ASYNC";

//액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

//순수 객체 반환
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });

//saga
function* increaseSaga() {
  //리덕스 미들웨어에 특정 작업을 명령하려면 effects(dalay, put 등) 를 yield 하면 된다.
  yield dalay(1000);
  //increase() 호출 -> increase 액션개체 생성 -> 미들웨어에 디스패치 명령(put)
  yield put(increase());
}

function* decreaseSaga() {
  yield dalay(1000);
  yield put(decrease());
}

이제 액션을 모니터링해 INCREASE_ASYNC, DECREASE_ASYNC 액션이 디스패치 된 경우 사가함수에 있는 코드들이 실행되게 해야 되는데 이를 위해 먼저 상단에서 takeEverytakeLatest 를 불러와야 한다.

takeEvery 는 첫번째 파라미터의 액션이 디스패치되면 두번째 파라미터로 받은 함수를 호출 해준다.

takeLatest 는 가장 마지막에 들어온 것만 처리해주는데 예를 들어 delay 가 있는 동안 다시 명령이 들어오면 기존의 것은 무시하고 마지막것만 처리하도록한다.

import { delay, put, takeEvery, takeLatest } from "redux-saga/effects";

//...
//후에 rootReducer 을 만들듯이 rootSaga 를 만들기 위해 export
export function* counterSaga() {
  //INCREASE_ASYNC 가 디스패치 되면 increaseAsync() 실행
  yield takeEvery(INCREASE_ASYNC, increaseSaga);
  //디스패치된 DECREASE_ASYNC 중 가장 마지막에 디스패치된 것만 실행
  yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}

이제 modules/index.js 에서 rootSaga 를 만들어주자.

//modules/index.js
import { combineReducers } from "redux";
import counter, { counterSaga } from "./counter";
import posts from "./posts";
import { all } from "redux-saga/effects";

const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
  //다른 사가를 추가할 경우 배열 내부에 계속 등록해주면 된다.
  yield all([counterSaga()]);
}

export default rootReducer;

이제 루트의 index.js 에서

//index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import rootReducer, { rootSaga } from "./modules";
import logger from "redux-logger";
import { composeWithDevTools } from "redux-devtools-extension";
import ReduxThunk from "redux-thunk";
import { Router } from "react-router-dom";
import { createBrowserHistory } from "history";
import createSagaMiddleware from "redux-saga";

const customHistory = createBrowserHistory();

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  rootReducer,
  //logger 와 다른 미들웨어를 같이 사용시 logger 가 마지막에 와야한다.
  composeWithDevTools(
    applyMiddleware(
      //withExtraArgument : Thunk 함수에서 3번째 파라미터로 넣은 값을 불러올 수 있도록 도와준다.
      ReduxThunk.withExtraArgument({ history: customHistory }),
      sagaMiddleware,
      logger
    )
  )
);

//store 밑에서
sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <React.StrictMode>
    <Router history={customHistory}>
      <Provider store={store}>
        <App />
      </Provider>
    </Router>
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

위와 같이 설정해주면 된다.

//createSagaMiddleware 를 불러와 주고 
import createSagaMiddleware from "redux-saga";

//변수에 담고
const sagaMiddleware = createSagaMiddleware();

//스토어에 넣어준후
const store = createStore(
  rootReducer,
  //logger 와 다른 미들웨어를 같이 사용시 logger 가 마지막에 와야한다.
  composeWithDevTools(
    applyMiddleware(
      //withExtraArgument : Thunk 함수에서 3번째 파라미터로 넣은 값을 불러올 수 있도록 도와준다.
      ReduxThunk.withExtraArgument({ history: customHistory }),
      //사가와 덩크의 순서는 상관 없다.
      sagaMiddleware,
      logger
    )
  )
);

//Run!
sagaMiddleware.run(rootSaga);

이제 App.js 에 컴포넌트를 넣고 확인해 보자.

+를 누르면 1초후에 작동하고 연속으로 누르면 누른 시점 기준으로 1초후에 계속해서 올라가는 것을 볼 수 있다.

-를 여러번 연속으로 눌렀을 때는 마지막 것만 작동하고 그 이전에 delay 중이던 작업들은 작동되지 않는 것을 확인할 수 있다.