programming language/react

[React] Redux-saga 알아보기

공대키메라 2022. 6. 27. 22:15

지난 시간에 Redux-thunk에 대해 알아보았다.

(궁금하면 여기 클릭!)

 

이번 시간에는 Redux-thunk와는 좀 다른 Redux-saga에 대해서 알아보겠다.

 

참고한 사이트는 다음과 같다.

 

참고:

https://redux-saaga.js.org/

https://kyounghwan01.github.io/blog/React/redux/redux-saga/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2

https://react.vlpt.us/redux-middleware/10-redux-saga.html

 

1. redux-saga란?

공식 사이트에서 redux-saga를 다음과 같이 소개하고 있다.

 

redux-saga는 어플리케이션의 side effects를 더 실행하기 효율적으로, 시험하기 쉽도록, 그리고 실패를 더 잘 관리하도록 만드는데 목적을 둔 라이브러리(library)다.

mental model( 뜻 : 사용자가 제품을 이해하는 방식)은 saga가 side effects들에 홀로 책임이 있는 어플리케이션의 분리된 thread(쓰레드)같다는 것이다. 

이게 무슨 말이냐면, 이 쓰레드가 보통 redux action에서 주요한 어플리케이션에서 시작되고, 멈추고, 취소될 수 있다는 말이며 모든 redux 어플리케이션 상태에 접근하고 또한 redux action을 dispatch할 수 있다는 말이다. 

redux-saga는 비동기적인 흐름을 읽고, 쓰고, 테스트하는걸 쉽게 해주는 Generator라고 불리는 ES6 문법을 사용한다. 

이렇게 하면, 이 비동기적인 흐름들은 보통 동기적 자바스크립트 코드처럼 보인다. 

redux-thunk를 전에 사용했을 텐데, 이와 달리 callback hell에 빠지지 않고, 쉽게 비동기 흐름을 테스트 할 수 있으며 action을 순수하게 유지할 수 있다.

 

redux-saga는 비동기적으로 dispatch를 사용할 수 있으며(put),

 

내부 메소드를 활용하여, 사용자의 부주의로 인하여 동일한 api를 여러번 req할 경우 가장 최근 or 가장 마지막(takeleast) req의 res만 받아오도록 하는 기능도 있습니다. (thuttle, debounce)

 

여기서 generator를 사용한다는 말이 있는데, generator는 es6에서 새롭게 생긴 기능이다. 

 

그럼 이게 정확히 무엇인지 먼저 알아야한다.

2. ES6 generator 알아보기

generator function은 사실 유용한 js 편에서 키메라가 사알짝 소개했엇다.(모르면 어쩔 수 없고 또르르...)

 

generator 함수는 중단점이 있는 함수이다. 

 

const gen = function *(){
    console.log(1)
    yield;
    console.log(2)
    yield;
    console.log(3)
    yield;
    yield 4
}

const generator = gen();

generator.next() // 1

generator.next() // 2

generator.next() // 3

generator.next() // {value: 4, done: false}

generator.next() // {value: undefined, done: true}

 

이렇게 중단점이 있는 함수로 간단하게  generator함수에 대해 알아보았다.

3. redux-saga 적용하기 

필자는 새로운 boilder plate에 redux-saga를 생성해서 적용했다. 

 

redux-thunk를 적용했던 기존 프로젝트에서 이를 적용하고 싶다면 redux-thunk를 삭제하면 될 것이다. 

 

npm i react-redux redux
npm rm redux-thunk // redux-thunk 삭제 명령어
npm i redux-saga // redux-saga 설치 

 

물론 구현할 기능은 동일하다. 다만 거기에 좀 더 추가되었다. 

 

파일 목록은 다음과 같다. 

App.js

import "./App.css";
import Counter from "./Counter";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Counter />
      </header>
    </div>
  );
}

export default App;

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import rootReducer, { rootSaga } from "./sagas";
import { composeWithDevTools } from "redux-devtools-extension";
import createSagaMiddleware from "redux-saga";
import logger from "./myLogger/myLogger";
const sagaMiddleware = createSagaMiddleware();

const middlewares = [logger, sagaMiddleware];
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(...middlewares))
);
sagaMiddleware.run(rootSaga); // 루트 사가를 실행

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

 

redux-saga도 redux-thunk와 동일한 미들웨어이다. 미들웨어는 하나만 적용할 수 있는 것은 아니고 여러개를 배열에 적용하길 원하는 미들웨어를 담아서 applyMiddleware의 parameter로 넘겨주면 된다. 

 

필자는 기존에 작성했던 log 기능의 모듈을 중간에 꼽사리(?) 끼워줬다.

 

여기서 composeWithDevTools가 있는데, redux-devtools-extention을 npm 명령어로 설치해주면...

 

우리는 이제 redux에서 실행한 action의 history를 확인할 수 있다.

 

 

크롬에서 redux-devtool를 다운받으면 된다. 

 

확인 외에도 특성 action 으로 쉽게 넘어갈 수도 있으니 개발할 때 사용하면 정말 좋다. 

 

redux-devtool 다운 받고 사용중인 화면

Counter.js

import { Button, InputNumber } from "antd";
import "antd/dist/antd.css";
import { useDispatch, useSelector } from "react-redux";
import {
  addNumberAction,
  addNumberAsyncAction,
  divNumberAction,
  mulNumberAction,
  subNumberAction,
  subNumberAsyncAction,
} from "./sagas/number";

const Counter = () => {
  const number = useSelector((state) => state.number.number);
  const dispatch = useDispatch();

  return (
    <>
      <div style={{ color: "white" }}>{number}</div>
      <div>
        <InputNumber />
        <Button type="primary" onClick={() => dispatch(addNumberAction(2))}>
          add number
        </Button>
        <Button
          type="primary"
          style={{ marginLeft: 20, marginRight: 20 }}
          onClick={() => dispatch(addNumberAsyncAction(2))}
        >
          add async number
        </Button>
        <Button type="danger" onClick={() => dispatch(subNumberAction(2))}>
          substract number
        </Button>
        <Button
          type="primary"
          style={{ marginLeft: 20, marginRight: 20 }}
          onClick={() => dispatch(subNumberAsyncAction(2))}
        >
          substract async number
        </Button>
        <Button
          style={{ marginRight: 20 }}
          type="primary"
          onClick={() => dispatch(mulNumberAction(2))}
        >
          multiply number
        </Button>
        <Button type="danger" onClick={() => dispatch(divNumberAction(2))}>
          divide number
        </Button>
      </div>
    </>
  );
};

export default Counter;

sagas/index.js

import { combineReducers } from "redux";
import number, { numberSaga } from "./number";
import { all } from "redux-saga/effects";

const rootReducer = combineReducers({ number });
export function* rootSaga() {
  yield all([numberSaga()]); // all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}

export default rootReducer;

 

사실 여기서 combineReducer를 사용하지 않아도 된다고 한다. 추후에 다시 redux-saga에 대해 정리하고 깔끔하게 적요할 예정이다(잘 모르겟다능...)

sagas/number.js

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

const initialState = {
  number: 0,
};

const ADD_ASYNC = "ADD_ASYNC";
const SUB_ASYNC = "SUB_ASYNC";
const ADD_NUMBER = "ADD_NUMBER";
const SUB_NUMBER = "SUB_NUMBER";
const MUL_NUMBER = "MUL_NUMBER";
const DIV_NUMBER = "DIV_NUMBER";

export const addNumberAction = (data) => ({ type: ADD_NUMBER, data });
export const subNumberAction = (data) => ({ type: SUB_NUMBER, data });
export const addNumberAsyncAction = (data) => ({ type: ADD_ASYNC, data });
export const subNumberAsyncAction = (data) => ({ type: SUB_ASYNC, data });
export const mulNumberAction = (data) => ({ type: MUL_NUMBER, data });
export const divNumberAction = (data) => ({ type: DIV_NUMBER, data });

function* increaseSaga(state) {
  yield delay(1000); // 1초를 기다립니다.
  yield put(addNumberAction(state.data)); // put은 특정 액션을 디스패치 해줍니다.
}
function* decreaseSaga(state) {
  yield delay(1000); // 1초를 기다립니다.
  yield put(subNumberAction(state.data)); // put은 특정 액션을 디스패치 해줍니다.
}

export function* numberSaga() {
  yield takeEvery(ADD_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
  yield takeLatest(SUB_ASYNC, decreaseSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_NUMBER:
      return {
        ...state,
        number: state.number + action.data,
      };
    case SUB_NUMBER:
      return {
        ...state,
        number: state.number - action.data,
      };
    case MUL_NUMBER:
      return {
        ...state,
        number: state.number * action.data,
      };
    case DIV_NUMBER:
      return {
        ...state,
        number: state.number / action.data,
      };
    default:
      return state;
  }
};

export default reducer;

 

redux-saga를 쓰는, redux-thunk와 비교되는 가장 큰 이유는 지원해주는 기능이 많다는 것이다.

 

action을 내부에서 생성하면서, 이것들을 우리의 기호에 맞게 조작할 수 있다. 

 

예를 들어, takeLatest하는 기능이 있는데, 이것을 사용하면 수많은 action이 짧은 시간안에 넘어와도 최종 action만 실행하도록 한다.

 

이러한 것들을 saga의 effects들이라고 하는데, 다음 사이트에서 이에 대하여 확인할 수 있다.

 

https://redux-saga.js.org/docs/api

 

완성한 화면은 다음과 같다. 

 


이번 시간에는 redux-saga에 대해 알아보았다. 

 

생각보다 이게 익숙치가 않아서 사용하는데에 쉽지는 않았다. 

 

다음 시간에는 어떤 effect가 있는지 좀 더 자세히 알아볼 것이다.