Redux toolkit Saga

With·2021년 9월 15일
1

동작 과정

  1. 화면에서 disaptch({type : ... })
  2. 리덕스 모듈의 watch함수에서 dispatch된 type 감지
  3. saga effect로 감지된 타입의 worker 함수 실행
  4. worker 함수에서 API 요청 및 필요한 작업 진행(비동기작업)
    try ... catch 문으로 요청 성공 및 실패에 따른 후속 put 실행
    제너레이터 함수내에서 yield 된 순서에 따라 put (dispatch) 됨
  5. 리듀서에서 put 된 타입에 따른 새로운 store state 가 리턴
  6. 새로운 state를 화면단에서 useSelect으로 사용

Saga 구성요소

Watch함수, Worker 함수, 미들웨어 설정 (run)

1. Watch 함수

// module/todos.js
export function* watchTodos() {
  yield takeLatest(loadTodos().type, __loadTodos);
  yield takeLatest(loadTodoById().type, __loadTodoById);
}
  • Redux-saga는 타입을 관찰하고 있다가, 해당 타입이 dispatch되면, 원하는 작업을 실행시켜준다.
  • 위 예시에서는 function* watchTodos()loadTodos().typeloadTodoById().type을 관찰하고 있으며 만약 UI 에서 action typedispatch되면 각각 __loadTodos, __loadTodoById가 실행된다.
  • 위 예시에서 쓰인 saga effecttakeLatest 이며, takeLatest(액션타입, work 함수)으로 작동된다.

2. Worker 함수

// module/todos.js
function* __loadTodoById(action) {
  try {
    const { data: todo } = yield call(todoApi.loadTodo, action.payload);
    yield put(loadTodoByIdSuccess(todo));
  } catch (err) {
    yield put(loadTodoByIdFail(err));
  }
}

function* __loadTodos(action) {
  try {
    const { data: todo } = yield call(todoApi.loadTodo, action.payload);
    yield put(loadTodoByIdSuccess(todo));
    const { data: todos } = yield call(todoApi.loadTodos);
    yield put(loadTodosSuccess(todos));
  } catch (err) {
    yield put(loadTodosFail(err));
  }
}
  • 위 예시에서는 Todos 와 Todo를 요청하는 동작 2개가 있으며, watch에서 관측되면 실행된다. 각각의 함수에서 try...catch(e)를 통해 에러 처리를 해주고, API 요청 성공과 실패에 해당하는 후속작업을 put해준다.
  • put은 redux-thunk에서 dispatch와 같은 기능을 한다.

3. watch 함수 모으기 & saga run

export function* rootSaga() {
  yield all([watchTodos()]);
}

const store = configureStore({
	// ... 중략
})

sagaMiddleWare.run(rootSaga); // 항상 store 선언 이후에 위치해야 한다.

export default store;
  • rootSaga 라는 이름으로 watch 함수들을 모아준다. (꼭 rootSaga가 아니어도 됨)
  • 만들어진 rootSaga는 run에서 파라미터로 넣어진다.

전체코드

1. module/todos.js

import { createSlice } from "@reduxjs/toolkit";
import { call, put, takeLatest, delay } from "redux-saga/effects";
import { todoApi } from "../api";

const initialState = {
  todos: {
    loading: true,
    error: null,
    todos: [],
  },
  todo: {
    loading: true,
    error: null,
    todo: {},
  },
};

// Slice
const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    loadTodos: (state) => {
      state.todos.loading = true;
    },
    loadTodosSuccess: (state, { payload }) => {
      state.todos.loading = false;
      state.todos.todos = payload;
    },
    loadTodosFail: (state, { payload }) => {
      state.todos.loading = false;
      state.todos.error = payload;
    },
    loadTodoById: (state) => {
      state.todo.loading = true;
    },
    loadTodoByIdSuccess: (state, { payload }) => {
      state.todo.loading = false;
      state.todo.todo = payload;
    },
    loadTodoByIdFail: (state, { payload }) => {
      state.todo.loading = false;
      state.todo.error = payload;
    },
  },
});

export const {
  initTodos,
  loadTodos,
  loadTodosSuccess,
  loadTodosFail,
  loadTodoById,
  loadTodoByIdSuccess,
} = todoSlice.actions;

// loadTodoById
function* __loadTodoById(action) {
  delay(300); // 300ms 대기하고 나서 아래 명령 실행한다. (만약 300ms 이전 api 재요청 시 watch함수의 takeLatest에 의해 이전 요청이 취소되고 요청을 다시 시도함. 즉, 이렇게 'debounce'를 구현할 수 있음, 300ms 이전에 dispatch를 여러번 해도 결국 300ms가 지나고나서 단 1번의 네트워크 요청만 한다.)
  try {
    const { data } = yield call(todoApi.loadTodo, action.payload);
    yield put(loadTodoByIdSuccess(data));
  } catch (e) {}
}

// 👇 화면(App.js)에서 dispatch(actionCreator(action.payload)) 에서 보낸 payload를 받아 올 수 있다.
function* __loadTodos(action) {
  try {
    // 👇 call(api요청문, api요청문에 들어갈 파라미터) 로 작성한다. (문법에 유의)
    const { data: todo } = yield call(todoApi.loadTodo, action.payload);
    yield put(loadTodoByIdSuccess(todo)); // 첫번째 dispatch
    const { data: todos } = yield call(todoApi.loadTodos);
    yield put(loadTodosSuccess(todos)); // 두번째 dispatch
  } catch (err) {
    yield put(loadTodosFail(err)); // api 요청 실패 시 
  }
}

// Saga Watcher : Type들을 감시하고 있음 (보통 watch--- 라고 짓더라)
export function* watchTodos() {
  yield takeLatest(loadTodos().type, __loadTodos);
  yield takeLatest(loadTodoById().type, __loadTodoById);
}

export default todoSlice;

2. module/index.js (configureStore.js)

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import { all } from "redux-saga/effects";
import logger from "redux-logger";

// Slice
import todosSlice from "./todos";

// Sagas
import { watchTodos } from "./todos";

//Saga set
const sagaMiddleWare = createSagaMiddleware();
export function* rootSaga() {
  yield all([watchTodos()]); // all([ ]) 은 모든 watch 함수를 실행시키는 것
}

const store = configureStore({
  reducer: {
    todos: todosSlice.reducer,
  },
  middleware: (getDefaultMiddleware) => [
    ...getDefaultMiddleware({ thunk: false }), // thunk 꺼주기
    sagaMiddleWare,
    logger,
  ],
});

sagaMiddleWare.run(rootSaga); // 항상 store 선언 이후에 위치해야 한다.
export default store;

3. App.js

import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { loadTodoById, loadTodos } from "./module/todos";

function App() {
  const dispatch = useDispatch();
  const { todos, loading, error } = useSelector((state) => state.todos.todos);
  const { todo } = useSelector((state) => state.todos.todo);

  // 테스트 삼아 1을 action.payload로 보냄
  useEffect(() => {
    dispatch(loadTodos(1));
  }, []);

  return (
    <div className="App">
      {todos.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
    </div>
  );
}

export default App;

4.api/index.js

import axios from "axios";

const instance = axios.create({
  baseURL: "https://jsonplaceholder.typicode.com",
});

export const todoApi = {
  loadTodos: () => instance.get("/posts"),
  loadTodo: (id) => instance.get(`posts/${id}`),
};
profile
주니어 프론트엔드 개발자 입니다.

0개의 댓글