'리액트를 다루는 기술' 18장, 리덕스 미들웨어를 통한 비동기 작업 관리(2/2)

Jake_Young·2020년 9월 9일
0
post-thumbnail

😎 비동기 작업을 처리하는 Redux-Saga

  • redux-thunk 다음으로 가장 많이 사용되는 미들웨어
  • 특정 액션이 디스패치 되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성

😁 Redux-Saga가 필요한 상황

  1. 기존 요청을 취소 처리해야 할 때(불필요한 중복 요청 방지)
  2. 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
  3. 웹소켓을 사용할 때
  4. API 요청 실패 시 재요청해야 할 때

🤩 제너레이터 함수 이해하기

  • redux-saga에서는 ES6의 제너레이터 함수라는 문법을 사용한다.
  • 제너레이터 함수는 함수에서 값을 순차적으로 반환할 수 있다.
  • 제너레이터 함수를 만들 때는 function* 키워드를 사용한다.
  • 제너레이터 함수에서 반환값은 return이 아닌 yield를 사용한다.
  • 호출할 때는 함수를 실행시켜 변수에 할당하고 그 함수에서 next 메소드를 실행시켜 부른다.
  • next 메소드에 파라미터를 넣으면 제너레이터 함수에서 yield를 사용하여 해당 값을 조회할 수 있다.
  • 제너레이션 함수를 여기서는 사가(saga)라고 부른다.

🥳 실전 코드 연습

  • 아래의 코드는 redux-thunk 기반 위에 덮어쓰거나 새로 만들면 된다.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer, { rootSaga } from "./modules";
import { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";
import creasteSagaMiddleware from "redux-saga";
import { composeWithDevTools } from "redux-devtools-extension";

const rootElement = document.getElementById("root");
const logger = createLogger();
const sagaMiddleware = creasteSagaMiddleware();

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(logger, ReduxThunk, sagaMiddleware))
);
sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

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

const rootReducer = combineReducers({
  counter,
  loading,
  sample
});

export function* rootSaga() {
  // all 함수는 여러 사가를 합쳐 주는 역할
  yield all([counterSaga(), sampleSaga()]);
}

export default rootReducer;

// src/modules/counter.js
import { createAction, handleActions } from "redux-actions";
import {
  delay,
  put,
  takeEvery,
  takeLatest,
  select,
  throttle
} 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 = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 마우스 클릭 이벤트가 payload 안에 들어가지 않도록
// () => undefined를 두 번째 파라미터로 넣어 준다.
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, () => undefined);

function* increaseSaga() {
  yield delay(1000); // 1초 기다리기
  yield put(increase()); // 액션 디스패치하기
  const number = yield select((state) => state.counter);
  console.log(`현재 값은 ${number}입니다`);
}

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

export function* counterSaga() {
  // takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리해 준다.
  yield throttle(3000, INCREASE_ASYNC, increaseSaga);
  // takeLatest는 기존에 진행 중이던 작업이 있다면 취소 처리하고
  // 가장 마지막으로 실행된 작업만 수행합니다.
  yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}

const initialState = 0;

const counter = handleActions(
  {
    [INCREASE]: (state) => state + 1,
    [DECREASE]: (state) => state - 1
  },
  initialState
);

export default counter;

// src/modules/sample.js
import { createAction, handleActions } from "redux-actions";
import * as api from "../lib/api";
import { call, put, takeLatest } from "redux-saga/effects";
import { startLoading, finishLoading } from "./loading";
import createRequestSaga from "../lib/createRequestSaga";

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";

const GET_USERS = "sample/GET_USER";
const GET_USERS_SUCCESS = "sample/GET_USER_SUCCESS";

export const getPost = createAction(GET_POST, (id) => id);
export const getUsers = createAction(GET_USERS);

const getPostSaga = createRequestSaga(GET_POST, api.getPost);
const getUsersSaga = createRequestSaga(GET_USERS, api.getUsers);

export function* sampleSaga() {
  yield takeLatest(GET_POST, getPostSaga);
  yield takeLatest(GET_USERS, getUsersSaga);
}

const initialState = {
  post: null,
  users: null
};

const sample = handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      post: action.payload
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      users: action.payload
    })
  },
  initialState
);

export default sample;

// src/modules/loading.js
import { createAction, handleActions } from "redux-actions";

const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";

// 요청을 위한 액션 타입을 payload로 설정한다 (예: "sample/GET_POST")

export const startLoading = createAction(
  START_LOADING,
  (requestType) => requestType
);

export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType
);

const initialState = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false
    })
  },
  initialState
);

export default loading;

// src/lib/createRequestSaga.js
import { call, put } from "redux-saga/effects";
import { startLoading, finishLoading } from "../modules/loading";

export default function createRequestSaga(type, request) {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;

  return function* (action) {
    yield put(startLoading(type));
    try {
      const response = yield call(request, action.payload);
      yield put({
        type: SUCCESS,
        payload: response.data
      });
    } catch (e) {
      yield put({
        type: FAILURE,
        payload: e,
        error: true
      });
    }
    yield put(finishLoading(type));
  };
}
profile
자바스크립트와 파이썬 그리고 컴퓨터와 네트워크

0개의 댓글