redux-toolkit은 redux에서 자주쓰는 기능을 모아둔 라이브러리이다. 매우 유용해서 요즘 많이 쓰이고 있음 redux-toolkit에는 thunk, immer, createAction도 모두 내장되어 있다. 따라서 코드량이 확 줄어든다! 🤩

툴킷을 선택한 이유

요즘에는 redux-saga → thunk + redux-toolkit을 조합하는 것으로 많이 전환하는 추세인 것 같다. saga에서 제공하는 다양한 이펙트들이 있으나, 주로 사용하는 기능은 takeLatest, takeEvery 정도이고, debounce나 throttle의 경우 lodash와 thunk와의 조합으로 충분히 구현해낼 수 있으므로 굳이 saga를 적용하지 않게되는 것이다. 또 제너레이터 문법에 익숙하지 않은 동료들과의 협업하는 관점에서도 고민되는 부분이 있는 것 같다.

위와 같은 이유로 지금까지 만들어왔던 react-redux 프로젝트를 Redux-toolkit으로 바꿔서 적용해보자.

먼저 툴킷을 이용하면 store 부터 굉장히 가벼워진다. 위에 적었다시피 toolkit이 이미 thunk나 immer들을 내장함수로 가지고 있기 때문이다. 따라서 기존 store의 코드에 redux-toolkit을 적용하면 아래와 같다.

 const { configureStore, getDefaultMiddleware } = require("@reduxjs/toolkit");

const reducer = require("./reducers");

const firstMiddleware = (store) => (dispatch) => (action) => {
  console.log("로깅:", action);
  dispatch(action);
};

const store = configureStore({
  reducer,
  middleware: [firstMiddleware, ...getDefaultMiddleware], // getDefaultMiddleware 기본 미들웨어 추가
  devTools: process.env.NODE_ENV !== "production",
  // enhancer
  // preloadedState, // SSR용 설정
});

module.exports = store;

createAsyncThunk로 비동기 처리하기

기존의 action도 action과 actionCreator를 분리하여 생성하던 이전 과정과는 달리 createAsyncThunk를 사용해서 간단하게 구현할 수 있다. redux-toolkit에서는 action에 대한 정의를 비동기 액션, 혹은 외부적 액션에 대한 것만 적어주면된다. (동기액션, 곧 컴포넌트 내부의 액션은 리듀서에서 정의하면 자동 생성된다)

actions/user.js

const { createAsyncThunk } = require("@reduxjs/toolkit");

const logIn = createAsyncThunk("user/logIn", async (data, thunkAPI) => {
	return await delay(500, { userId: 1, nickname: "vicky" });
});

module.exports = { logIn }

actions/post.js

const { createAsyncThunk } = require("@reduxjs/toolkit");

const addPost = createAsyncThunk("post/add", async (data, thunkAPI) => {
  return await delay(500, { title: "새 게시글", content: "내용이 들어간다" });
});

module.exports = { addPost };

createAsyncThunk 메서드 안에 첫번째 인자로 user/login, post/add등의 경로를 적어주는데 위와 같이 적어주면, 리듀서에서 정의한 pending - fulfilled - rejected라는 메서드가 뒤에 붙어 redux-devtools에서 데이터 변경 트래킹이 가능해진다.

새롭게 등장한 slice

reducers/index.js

reducer도 굉장히 가벼워지는데, userReducer, postReducer 대신 userSlice, postSlice를 사용한다.

const { combineReducers } = require("redux");
const userSlice = require("./user");
const postSlice = require("./post");

module.exports = combineReducers({
  user: userSlice.reducer,
  posts: postSlice.reducer,
});

먼저 user 리듀서를 구현해보면 아래와 같다.

reducers/user.js

const { createSlice } = require("@reduxjs/toolkit");
const { logIn } = require("../actions/user");

const initialState = {
  isLoggingIn: false,
  data: null,
};

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: { // 동기적 액션, 내부적 액션, userSlice.actions.logOut()으로 액션이 자동 생성됨
    logOut(state, action) {
      state.data = null;
    },
  },
  extraReducers: { // 비동기 액션, 외부적 액션 (pending, fulfilled, rejected로 사용)
    // user/logIn/pending
    [logIn.pending](state, action) {
      state.isLoggingIn = true;
    },
    // user/logIn/fulfilled
    [logIn.fulfilled](state, action) {
      state.data = action.payload;
      state.isLoggingIn = false;
    },
    // user/login/rejected
    [logIn.rejected](state, action) {
      state.data = null;
      state.isLoggingIn = false;
    },
  },
});

module.exports = userSlice;