앞서 구현한 Redux의 상태관리 로직은 Context API와 useReducer 훅을 사용한 방식과의 큰 차이점이 없다. Redux는 Context API에는 존재하지 않는 미들웨어(Middleware)가 존재한다. Middleware에 대해 학습하고 적용해보자.
즉, action -> middleware -> dispatch -> reducer 과정으로 액션이 디스페치되면 리듀서에서 해당 액션을 실행하기 전에 추가적인 작업을 수행한다.
이러한 특징은 특히 비동기 작업 처리에서 많이 사용된다.
// myLogger.ts
import { Action, AnyAction, Dispatch, Middleware, MiddlewareAPI } from 'redux';
import { IRootState } from '@/modules/types';
const myLogger: Middleware<
  Record<string, never>,
  IRootState,
  Dispatch<AnyAction>
> =
  (store: MiddlewareAPI<Dispatch<AnyAction>, IRootState>) =>
  (next: Dispatch<AnyAction>) =>
  (action: Action<string>) => {
    console.log(action); // 현재 디스패치되는 액션 출력
    const result = next(action); // 다음 미들웨어 (없다면 리듀서)에게 액션 전달
    console.log(store.getState()); // 리듀서에서 처리된 상태 출력
    return result; // dispatch(action)의 결과물 반환
  };
export default myLogger;
// root/index.ts
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import myLogger from './middlewares/myLogger';
const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myLogger),
});
export type AppDispatch = typeof store.dispatch;
미들웨어 안에서는 어떤 작업이든 가능하다.
미들웨어는 여러개를 만들어서 적용할 수 있다.
middleware: (getDefaultMiddleware) =>
  getDefaultMiddleware().concat(myLogger, anotherMiddleware),
redux-thunk, redux-saga가 많이 사용다.redux-thunk는 리덕스에서 비동기 작업을 처리 할 때 가장 많이 사용되는 미들웨어다.thunk(미들웨어)를 사용하여 apis 파일에서 비동기 처리하던 로직을 redux내에서 처리되도록 수정해보자.
// modules/users.ts
// 비동기 액션 (thunks) 함수
export const getUsersData = createAsyncThunk(
  SET_USERS,
  async (_, { rejectWithValue }) => {
    try {
      const res = await axiosInstance.get<IUser[]>('/user_data');
      return res.data;
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);
export const addUserData = createAsyncThunk(
  ADD_USER,
  async (user: IUser, { rejectWithValue }) => {
    try {
      await axiosInstance.post<IUser>('/user_data', user);
      return user;
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);
export const updateUsersData = createAsyncThunk(
  UPDATE_USERS,
  async (
    { ids, updateValue }: { ids: number[]; updateValue: boolean },
    { rejectWithValue },
  ) => {
    try {
      const userToUpdate = { isDeleted: updateValue };
      const queryString = ids.join(',');
      await axiosInstance.patch(`/user_data?ids=${queryString}`, userToUpdate);
      return { ids, updateValue };
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);
// slice 생성 (reducer)
const usersSlice = createSlice({
  name: 'users',
  initialState: initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getUsersData.pending, (state) => {
        state.loading = true;
      })
      .addCase(getUsersData.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(getUsersData.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || null;
      })
      .addCase(addUserData.pending, (state) => {
        state.loading = true;
      })
      .addCase(addUserData.fulfilled, (state, action) => {
        state.loading = false;
        state.users.push(action.payload);
      })
      .addCase(addUserData.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || null;
      })
      .addCase(updateUsersData.pending, (state) => {
        state.loading = true;
      })
      .addCase(updateUsersData.fulfilled, (state, action) => {
        state.loading = false;
        const { ids, updateValue } = action.payload;
        state.users = state.users.map((user) => {
          if (ids.includes(user.id)) {
            return { ...user, isDeleted: updateValue };
          }
          return user;
        });
      })
      .addCase(updateUsersData.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || null;
      });
  },
});
middleware다.middleware 덕분에 액션은 Promise 결과를 기다리고 리듀서에 전달 될 수 있다.Promise의 pending, fulfilled, rejected의 상태에 따른 활용도 가능하다.dispatch를 호출하는 방법도 동일한 동작을 지원했으나middleware를 이용하여 비즈니스 로직을 중앙집중화하는게 적합하다.