[React] Redux-toolkit에서 미들웨어 사용하기

Suyeon·2021년 2월 25일
7

React

목록 보기
25/26

Redux-toolkit에서 redux-thunk / redux-saga를 사용해서 비동기 작업 처리하기 🤓

Redux-thunk

  • Redux-thunk는 promise를 반환한다. -> 디버깅이 어려움
  • 비교적 간단한 syntax -> 간단한 프로젝트, 비기너에 적합

Redux-toolkit에는 기본적으로 thunk가 내장되어있어서, 별도의 설치없이 사용할 수 있다.
Codesandbox 참고

예제

store.js

// redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice";

export default configureStore({
  reducer: {
    users: userReducer
  }
});

index.js
Provider로 전체 컴포넌트 감싸기

// index.js
import { Provider } from "react-redux";
import store from "./redux/store";
// omit other codes...

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

userSlice.js

Slice 생성

// redux/userSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

export const fetchUser = createAsyncThunk("users/fetchUser", async () => {
  return axios
    .get("url")
    .then((res) => res.data)
    .catch((error) => error);
});

const usersSlice = createSlice({
  name: "users",
  initialState: {
    users: [],
    loading: false,
    error: ""
  },
  reducers: {},
  extraReducers: {
    [fetchUser.pending]: (state) => {
      state.loading = true;
      state.users = [];
      state.error = "";
    },
    [fetchUser.fulfilled]: (state, action) => {
      state.users = action.payload;
      state.loading = false;
      state.error = "";
    },
    [fetchUser.rejected]: (state, action) => {
      state.loading = false;
      state.users = [];
      state.error = action.payload;
    }
  }
});

export default usersSlice.reducer;

컴포넌트에서 사용하기

import { useSelector, useDispatch } from "react-redux";
import { fetchUser } from "./redux/userSlice";

export default function App() {
  const dispatch = useDispatch();
  const { users, loading, error } = useSelector((state) => state.users);

  if (error) {
    return <p>Something went wrong! please, try again.</p>;
  }

  if (loading) {
    return <p>Loading</p>;
  }

  return (
    <div className="App">
      <h1>Fetch user data</h1>
      <button onClick={() => dispatch(fetchUser())}>Get users</button>
      {users?.length > 0 &&
        users.map((user) => <div key={user.id}>{user.name}</div>)}
    </div>
  );
}

Redux-saga

a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

ES6의 Generator function 사용

  • 비동기 코드이지만, 동기적 코드처럼 보이는 clean code (async/await 처럼) 구현
  • 위와 같은 이유로 callback hell을 피할 수 있음
  • action에 대해서 세부적인 컨트롤이 가능함

Thunk말고 다른 미들웨어를 사용할 경우, 이전(기존의 redux)과 비슷하게 적용하면 된다.

  • saga: sideEffect를 가진 Generator Function
  • fork: 동기적으로 함수 호출
  • call: 비동기적으로 함수 호출 (saga안에서 sideEffect를 가진 함수를 직접 호출X)
  • put: Dispatch

callput을 사용해서, sideEffect를 가진 로직을 완전히 분리한다. 따라서 디버깅과 테스팅이 수월하다.

예제

Codesandbox 참고

store.js

// redux/store.js
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import userReducer from "./userSlice";
import { watcherSaga } from "./sagas/rootSaga";

const reducer = combineReducers({
  users: userReducer
  // others...
});

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
  reducer: reducer,
  middleware: [sagaMiddleware]
});

sagaMiddleware.run(watcherSaga); // Listener

export default store;

index.js
Provider로 전체 컴포넌트 감싸기

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

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

rootSaga.js
rootSaga를 생성해서 여러개의 saga를 합친다.
아래의 경우, 컴포넌트에서 getUser.typedispatch하면 handleGetUser를 실행한다.

// redux/sagas/rootSaga.js
import { takeLatest } from "redux-saga/effects";
import { handleGetUser } from "../sagas/userSaga";
import { getUser } from "../userSlice";

export function* watcherSaga() {
  yield takeLatest(getUser.type, handleGetUser);
}

만약 여러개의 sagawatching하고 싶다면, 아래와 같이 작성한다.

export function* watcherSaga() {
  yield [
    takeLatest(getUser.type, handleGetUser)
    takeLatest(something.type, logout)
  ]
}

userSaga.js
User의 데이터를 fetch하는 로직을 다루는 saga 생성하기

// redux/sagas/userSaga.js
import { call, put } from "redux-saga/effects";
import { setUser, failedGetUser } from "../userSlice";
import { fetchUser } from "../../api/api";

export function* handleGetUser() {
  try {
    const res = yield call(fetchUser);
    yield put(setUser(res)); 
  } catch (error) {
    yield put(failedGetUser(error));
  }
}

api.js
Fetch 로직 작성

// api/api.js
import axios from "axios";

export const fetchUser = async () => {
  return axios
    .get("https://jsonplaceholder.typicode.com/users")
    .then((res) => res.data)
    .catch((error) => error);
};

userSlice.js

// redux/userSlice.js
import { createSlice } from "@reduxjs/toolkit";

const userSlice = createSlice({
  name: "user",
  initialState: {
    users: [],
    loading: false,
    error: ""
  },
  reducers: {
    getUser(state) {
      state.loading = true;
    },
    setUser(state, action) {
      state.users = action.payload;
      state.loading = false;
    },
    failedGetUser() {
      state.error = action.payload;
      state.loading = false;
    }
  }
});

export const { getUser, setUser, failedGetUser } = userSlice.actions;

export default userSlice.reducer;
profile
Hello World.

3개의 댓글

comment-user-thumbnail
2021년 4월 2일

안녕하세요 글 잘 봤습니다
다름이 아니고 질문이 있습니다

export const fetchUser = async () => {
return axios
.get("https://jsonplaceholder.typicode.com/users")
.then((res) => res.data)
.catch((error) => error);
};
async 예약어 쓰시고 await 가 아닌 이유가 따로 있으신가요?

1개의 답글
comment-user-thumbnail
2022년 4월 26일

좋은 자료 덕분에 도움이 많이 됐습니다. ^^

답글 달기