Redux - middleware

sarang_daddy·2023년 11월 26일
0

Redux

목록 보기
2/3

Redux에 미들웨어 추가하기

앞서 구현한 Redux의 상태관리 로직Context APIuseReducer 훅을 사용한 방식과의 큰 차이점이 없다. Redux는 Context API에는 존재하지 않는 미들웨어(Middleware)가 존재한다. Middleware에 대해 학습하고 적용해보자.

1. 미들웨어란?

  • Redux의 미들웨어를 사용하면 액션 객체가 리듀서에서 처리되기 전에 다른 작업을 수행할 수 있다.

즉, action -> middleware -> dispatch -> reducer 과정으로 액션이 디스페치되면 리듀서에서 해당 액션을 실행하기 전에 추가적인 작업을 수행한다.

2. 미들웨어에서 사용하는 다양한 작업들

  • 특정 조건에 따라 액션이 무시되게 만든다.
  • 액션이 디스패치 됐을 때 이를 수정해서 리듀서에 전달되도록 한다.
  • 특정 액션이 발생했을 때 특정 함수를 실행되도록 한다.

이러한 특징은 특히 비동기 작업 처리에서 많이 사용된다.

3. 미들웨어 만들어보기

// 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),

4. redux-thunk 프로젝트에 적용하기

  • redux 비동기 작업으로는 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;
      });
  },
});

5. 미들웨어 사용후 느낀점

  • Redux의 기본적인 액션은 동기적인 페이로드를 전달하는 객체다.
  • 즉, 리듀서에 전달되었을 때 최종 값을 가지고 있어야한다.
  • 하지만, 데이터 요청과 같은 비동기 작업은 응답을 기다려야 한다.
  • 이를 위해 사용되는 것이 middleware다.
  • middleware 덕분에 액션은 Promise 결과를 기다리고 리듀서에 전달 될 수 있다.
  • Promisepending, fulfilled, rejected의 상태에 따른 활용도 가능하다.
  • 기존의 비동기 함수 내부에서 dispatch를 호출하는 방법도 동일한 동작을 지원했으나
  • 애플리케이션의 상태를 예측 가능하게 만들자는 Redux 철학에는 middleware를 이용하여 비즈니스 로직을 중앙집중화하는게 적합하다.
profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글