본문 바로가기

Redux

20210623 Redux06 : redux-saga, ES06 Generator 함수 문법, redux-saga 세팅&활용, redux-actions

Redux 06





redux-saga


  • 미들웨어
  • 제너레이터 객체를 만들어 내는 제너레이터 생성 함수를 이용
  • 리덕스 사가 미들웨어를 설정 -> 자신이 만든 사가 함수 등록 -> 사가 미들웨어 실행 -> 등록된 사가함수를 실행할 액션을 디스패치 함
  • npm i redux-saga

  • redux-saga by 밸로퍼트
    • 아래 부터는 밸로퍼트님의 redux-saga를 참고하여 작성 하였습니다.
  • redux-thunk가 하지 못하는 일을 redux-saga가 할수 있게 해줌
    • 비동기 작업을 할 때 기존 요청 취소 처리
    • 특정 액션 발생시 다른 액션이 디스패치 되게처리 또는 JS 코드 실행 처리
    • API 요청이 실패 했을 때 재요청 작업 가능
  • 등의 까다로운 비동기 작업들에서 사용



Generator 함수


  • Generator 함수의 경우, 여러개의 return을 가지며 여러번에 걸쳐 return이 가능함
  • 순차적 반환, 함수 흐름중 정지 또는 그 자리에서 다시 진행 가능
  • function* 키워드 사용
  • 제너레이터 : 제너레이터 함수 호출시 반환 되는 객체
  • 제너레이터의 next() 함수를 통해서 코드가 실행되고, yield를 한값을 반환하고, 정지함
    • 이어서 다시 실행 시킬려면 next()를 다시 사용하면 됨
  • 또한 next()에 인자를 넣어 사용하면, 코드가 실행 될때 마다 필요한 인자를 활용해서 코드를 실행 시킬 수 있음
  • next()에서 받은 인자는 yield 로 들어가고, 실행이 정지됨
function* sumGenerator() {
  console.log("sumGenerator이 시작됐습니다.");
  let a = yield;
  console.log("a값을 받았습니다.");
  let b = yield;
  console.log("b값을 받았습니다.");
  yield a + b;
}

const sum = sumGenerator();
sum.next();
// sumGenerator이 시작됐습니다.
sum.next(1);
// a 값을 받았습니다.
sum.next(2);
// b 값을 받았습니다.
// {value: 3, done: false}

Generator로 액션 모니터링 하기


  • while true를 넣음으로서 계속 반복하면서 정지하고 있는 형태가 됨
function* watchGenerator() {
  console.log("모니터링 시작!");
  while (true) {
    const action = yield;
    if (action.type === "HELLO") {
      console.log("안녕하세요?");
    }
    if (action.type === "BYE") {
      console.log("안녕히가세요.");
    }
  }
}

const watch = watchGenerator();
watch.next();
// 모니터링 시작!
watch.next({ type: "HELLO" });
// 안녕하세요?
watch.next({ type: "BYE" });
// 안녕히가세요.



redux-saga 세팅 및 활용


redux-saga middleware setting


  • applyMiddleware에 createSagaMiddleware()로 만든 객체인, sagaMiddleware를 추가함
  • sagaMiddleware 객체에 있는 run() 함수에 Saga 함수를 모은 rootSaga를 넣어 실행
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
import { routerMiddleware } from "connected-react-router";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./modules/rootSaga";

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  composeWithDevTools(
    applyMiddleware(
      thunk.withExtraArgument({ history }),
      promise,
      routerMiddleware(history),
      sagaMiddleware
    )
  )
);

sagaMiddleware.run(rootSaga);

export default store;

redux-saga 함수 만들기


  • saga의 경우 Watcher와 Worker로 구성되어 있음
    • redux-saga by javarouka
    • redux-saga by sustainable-dev
    • redux-saga by mskim
    • Wathcher
      • saga 함수의 경우 비동기 작업 함수와 액션 타입을 연결해줌으로 서
      • action의 상태를 subscribe 하는 역할을 함
    • Worker
      • 실제 작업을 수행 하는 부분
    • saga-effect
      • 미들웨어에 의해 수행되는 명령을 담고 있는 js 객체
      • 모든 effect 들은 반드시 yield와 함께 사용해야 함
      • 블럭 : 해당 코드가 다르 조건으로 블럭이 해지되기 전에는 다음 라인 코드가 실행 되지 않음
        • put : 특정 Action을 middleware에서 dispatch 하게 하는 역할 (블럭X)
        • call : 해당 함수 작업에 대해서 middleware에서 실행시키게 함
          • 새로운 하위 saga 태스크를 생성하는 effect, 주로 promise 등의 실행, Ajax call (블럭O), resolve가 되어야 넘어감
        • all : 제너레이터 함수를 배열의 형태로 인자로 넣으면, 제너레이터 함수들이 병행적으로 동시 실행 -> resolve 될 때 까지 기다림 (Promise.all 과 비슷)
        • take : 특정 액션 감시 용도 (블럭O)
        • fork : 새로운 하위 saga 태스크를 생성하는 effect (블럭X)
        • select : redux state에서 특정 상태를 가져올 때 사용하는 effect (블럭O)
        • takeEvery : 들어오는 모든 액션에 대해 특정 작업 처리(특정 액션과 수행 함수를 연결)
import axios from "axios";
import { push } from "connected-react-router";
import { call, delay, put, takeEvery } from "redux-saga/effects";
// redux-saga

// redux-saga ActionCreator
const GET_USERS_SAGA_START = "GET_USERS_SAGA_START";
export function getUsersSagaStart() {
  return {
    type: GET_USERS_SAGA_START,
  };
}

// redux-saga 비동기 작업 함수 (Worker)
function* getUsersSaga(action) {
  try {
    yield put(getUsersStart());
    yield delay(2000); // 블록 O
    const res = yield call(axios.get, "https://api.github.com/users"); // 블록 O
    yield put(getUsersSuccess(res.data));
    yield put(push("/"));
  } catch (error) {
    yield put(getUsersFail(error));
  }
}

// Watcher (매번 사용하기 위해서, 태스크를 쌓아줌)
export function* usersSaga() {
  yield takeEvery(GET_USERS_SAGA_START, getUsersSaga);
  // 어떤 액션 타입에 의해서 어떤 사가 작업 함수가 발동할지 적음
}

rootSaga 만들기


  • all : 제너레이터 함수들이 병행적으로 동시 실행
import { all } from "redux-saga/effects";
import { usersSaga } from "./users";

export default function* rootSaga() {
  // saga를 모아서 하나로 시작하게 하는 부분
  yield all([usersSaga()]);
}

활용하기

  • 해당 작업 함수를 dispatch해서 작업 요청

import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersSagaStart } from "../redux/modules/users";

export default function UserListContainer() {
  const users = useSelector((state) => state.users.data);
  const dispatch = useDispatch();

  const getUsers = useCallback(() => {
    dispatch(getUsersSagaStart());
  }, [dispatch]);

  return <UserList users={users} getUsers={getUsers} />;
}



redux-actions



createAction 분석하기


import { createAction } from "redux-actions";

console.log(createAction("HELLO"));
// HELLO 함수가 return ->  f HELLO
console.log(createAction("HELLO")());
// HELLO type의 Action -> {type: 'HELLO'}
console.log(createAction("HELLO")("안녕하세요"));
// HELLO type, payload가 안녕하세요 인 Action -> {type: 'HELLO', payload: '안녕하세요'}

redux-actions를 통해서 actionCreator와 reducer 만들기


  • creatActions()를 통해서 type을 넣어 Action을 생성하고, ActionCreator 함수명은 알아서 만들어짐
  • 마지막에 {prefix: ""} 옵션을 넣으면, 자동으로 type에 앞에 해당 prefix를 붙여 ducks pattern에 맞는 type이름으로 만들어 줌
  • 구조 분해 할당 방식으로 해당 ActionCreator를 받아와 밖에서 자유롭게 사용 가능
  • reducer의 경우에는 , handleActions() 를 활용하여 변경할 해당 state 값을 변경
    • 첫번째 인자는 type에 따른 return 값 함수
    • 두번째 인자는 initialState 로 초기값
    • 세번째 인자는 들어오는 type에서 prefix를 거두어 내고, 로직 적용하기 위함으로 어떤 prefix인지를 넣음
// filter.js

import { createActions, handleActions } from "redux-actions";

// action Creator
export const { showAll, showComplete } = createActions(
  "SHOW_ALL",
  "SHOW_COMPLETE",
  {
    prefix: "redux-start/filter",
  }
);

// Initial Data structure
const initialState = "ALL";

// reducer
const reducer = handleActions(
  {
    SHOW_ALL: (state, action) => "ALL",
    SHOW_COMPLETE: () => "COMPLETE",
  },
  initialState,
  { prefix: "redux-start/filter" }
);

export default reducer;