앞서 구현한 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
를 이용하여 비즈니스 로직을 중앙집중화하는게 적합하다.