이번에는 Redux Toolkit과 Redux Saga를 함께 사용했다.

 

Toolkit이 잘 기억나지 않는다면 이전 글을 확인하고 와도 된다. 사실 별거 없다.

https://choisuhyeok.tistory.com/54

 

[Redux Toolkit] 지저분한 리덕스, RTK로 정리하자

리덕스를 쓰다 보면 정말 코드가 길어진다... 준비해야 할 코드도 많고 RTK로 조금 더 쉽고 깔끔하게 사용해보자아 사실 리덕스 안 쓴 지 오래돼서... 기억도 가물가물하다 내가 이해한 그대로 작

choisuhyeok.tistory.com

 

Redux Saga를 사용하는 이유

  1. Redux는 동기적으로 실행되기 때문에 비동기적 요청을 하기 위해서
  2. Redux Saga의 여러 메소드를 사용하기 위해서

 

예제에서는 비동기 요청은 하지 않고 delay를 사용해서 비동기를 나타내 보았다.

 

Saga에서 사용되는 Generator 문법

함수에 *를 붙이고, yield를 사용한다.

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

const number = num()
number.next() // 1
number.next() // 2
number.next() // 3
number.next() // 4

yield는 generator 함수의 실행을 일시 정지시키고 next()를 사용해서 진행할 수 있다.

Redux Saga에서 사용하는 함수 

delay

원하는 시간을 지연시킬 수 있다.

delay(1000) -> 1초 지연

delay(1000) // 1초 지연

 

put

액션을 dispatch 한다.

put({type: increment});

 

call

함수의 동기적인 호출을 할 때 사용한다.

첫 번째 인수는 함수, 나머지 인수는 해당 함수에 넣을 인수이다.

call(increment)

call과 put의 차이점은 put은 store에 인수로 들어온 action을 dispatch 하고 call은 주어진 함수를 실행한다.

 

takeEvery

들어오는 모든 액션을 실행한다.

takeEvery(increment, incrementSaga)

 

takeLatest

여러 액션이 실행될 때 가장 마지막의 액션만 실행한다.

요청을 실수로 2번 이상 실행했을 때 한 번만 가도록 할 수 있다.

takeLatest(decrement, decrementSaga)

 

all

generator 함수를 배열의 형태로 인수에 넣어주면 병렬방식으로 실행되고, 전부 완료될 때까지 기다린다.

all([incrementSaga(), decrementSaga()])

 

fork

함수의 비동기적인 호출을 할 때 사용한다.

all([fork(watchIncrement), fork(watchDecrement)]);

 

Redux Saga + Redux Toolkit으로 Counter 만들기

import { createSlice } from "@reduxjs/toolkit";

export const counterSlice = createSlice({
  name: "counter",
  initialState: {
    isLoading: false,
    isDone: false,
    number: 0,
    error: null,
  },
  reducers: {
    increment: (state) => {
      state.isLoading = true;
      state.isDone = false;
      state.error = null;
    },
    incrementSuccess: (state) => {
      state.isLoading = false;
      state.isDone = true;
      state.number = state.number + 1;
    },
    incrementFailure: (state, action) => {
      state.isLoading = false;
      state.isDone = false;
      state.error = action.error;
    },
    decrement: (state) => {
      state.isLoading = true;
      state.isDone = false;
      state.error = null;
    },
    decrementSuccess: (state) => {
      state.isLoading = false;
      state.isDone = true;
      state.number = state.number - 1;
    },
    decrementFailure: (state, action) => {
      state.isLoading = false;
      state.isDone = false;
      state.error = action.error;
    },
  },
});

export const {
  increment,
  incrementSuccess,
  incrementFailure,
  decrement,
  decrementSuccess,
  decrementFailure,
} = counterSlice.actions;

export default counterSlice.reducer;

먼저 createSlice를 이용해서 reducer, action을 만들어준다.

 

하나의 동작에 위의 reducers처럼 increment, incrementSuccess, incrementFailure로 비동기를 분기 처리

 

할 수 있도록 진입, 성공, 실패 3가지로 작성한다.

 

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import { counterSlice } from "./counter";
import rootSaga from "../sagas";

const rootReducer = combineReducers({
  counter: counterSlice.reducer,
});

const sagaMiddleware = createSagaMiddleware();

const createStore = () => {
  const store = configureStore({
    reducer: rootReducer,
    devTools: true,
    middleware: [sagaMiddleware],
  });

  sagaMiddleware.run(rootSaga);

  return store;
};

export default createStore;

 

store에 Saga를 붙여준다.

 

Redux Toolkit과 큰 차이점은 없다. middleware를 추가해주는 것이다.

import { call, delay, put, takeLatest } from "redux-saga/effects";
import {
  decrement,
  decrementFailure,
  decrementSuccess,
  increment,
  incrementFailure,
  incrementSuccess,
} from "../modules/counter";

function* incrementSaga() {
  try {
    yield call(increment);
    yield delay(1000);
    yield put({
      type: incrementSuccess,
    });
  } catch (err) {
    yield put({
      type: incrementFailure,
      error: err
    });
  }
}

export function* watchIncrement() {
  yield takeLatest(increment, incrementSaga);
}

function* decrementSaga() {
  try {
    yield call(decrement);
    yield put({
      type: decrementSuccess,
    });
  } catch (err) {
    yield put({
      type: decrementFailure,
      error: err
    });
  }
}

export function* watchDecrement() {
  yield takeLatest(decrement, decrementSaga);
}

비동기 작업을 진행하는 saga 함수를 만들어준다.

 

예제에서는 api 통신은 하지 않지만 increment를 할 때  delay를 사용해서 비동기처럼 보이도록 지연을 시켰다.

 

내가 느끼기엔 쉽게 생각해서 call로 api통신을 실행 한 뒤 put을 이용해 dispatch 하는 느낌을 받았다.

 

try ~ catch문을 통해서 성공과 실패를 따로 분기 처리 없이 할 수 있어서 좋은 것 같다.

import { all, fork } from "redux-saga/effects";
import { watchDecrement, watchIncrement } from "./counterSaga";

export default function* rootSaga() {
  yield all([fork(watchIncrement), fork(watchDecrement)]);
}

만들어 놓은 Saga함수를 하나로 만드는 rootSaga이다.

 

import { Provider } from "react-redux";
import { store } from "./store/store";

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

다른 middleware과 마찬가지로 Provider를 이용해서 store를 전해주면 전역 상태를 사용할 수 있다.

 

import React from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { decrement, increment } from "../modules/counter";

export default function Counter() {
  const number = useSelector((state) => state.number);
  const dispatch = useDispatch();
  return (
    <div>
      <div>
        <div>{number}</div>
        <button onClick={() => dispatch(increment())}>+1</button>
        <button onClick={() => dispatch(decrement())}>-1</button>
      </div>
    </div>
  );
}

+1 버튼을 클릭 시 delay를 시켜놨기 때문에 1초 뒤에 number상태가 변경되는 것을 확인할 수 있다.

-1 버튼은 클릭 직후 number상태가 변경된다.

'React Redux' 카테고리의 다른 글

[Redux Toolkit] 지저분한 리덕스, RTK로 정리하자  (2) 2022.01.23