Watch함수, Worker 함수, 미들웨어 설정 (run)
// module/todos.js
export function* watchTodos() {
yield takeLatest(loadTodos().type, __loadTodos);
yield takeLatest(loadTodoById().type, __loadTodoById);
}
dispatch
되면, 원하는 작업을 실행시켜준다.function* watchTodos()
가 loadTodos().type
과 loadTodoById().type
을 관찰하고 있으며 만약 UI 에서 action type
이 dispatch
되면 각각 __loadTodos
, __loadTodoById
가 실행된다.saga effect
는 takeLatest
이며, takeLatest(액션타입, work 함수)
으로 작동된다.// 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));
}
}
watch
에서 관측되면 실행된다. 각각의 함수에서 try...catch(e)를 통해 에러 처리를 해주고, API 요청 성공과 실패에 해당하는 후속작업을 put
해준다. put
은 redux-thunk에서 dispatch
와 같은 기능을 한다.export function* rootSaga() {
yield all([watchTodos()]);
}
const store = configureStore({
// ... 중략
})
sagaMiddleWare.run(rootSaga); // 항상 store 선언 이후에 위치해야 한다.
export default store;
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}`),
};