[Redux] Redux-Toolkit 시작하기!

Redux-thunk나 Redux-saga로 서버, 글로벌 상태를 관리하며 개발을 해오다,, 프로젝트 규모가 커질수록 점점 막대해지는 서버 상태로 방대해지는 반복 코드로 인하여 서버와 글로벌의 상태 분리가 필요하다고 느꼈고, 서버 상태관리를 위하여 react-query를 도입했다!

그리하여, 글로벌 상태 관리는 더이상 thunk나 saga가 적합하지 않고, 게다가 redux를 사용하면서 느꼈던 불편함들이 많았기 때문에..

  • 리덕스의 복잡한 스토어 설정
  • 리덕스를 유용하게 사용하기 위해서 추가되어야 하는 많은 패키지들
  • 리덕스 사용을 위해 요구되는 다량의 상용구(boilerplate) 코드

이러한 문제점을 보완하여 Redux에서는 Redux Toolkit을 만들어냈다!
이번 포스팅은 Redux Toolkit 공식문서와 Vercel에서 오픈한 Next.js + Typescript + Redux-Toolkit 소스코드를 참고하여 작성하였다.

Next.js + Typescript 설치

$ npx create-next-app 프로젝트명 --typescript

redux 패키지 설치

$ npm i @reduxjs/toolkit
$ npm i react-redux
$ npm i next-redux-wrapper
$ npm i @types/react-redux
$ npm i redux-logger # 필요한 경우에 설치

modules/counterSlice.ts

import { createSlice } from '@reduxjs/toolkit';

import type { AppState, AppThunk } from './store';

export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

// action + reducer 정의
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
  },
});

// useSelector에 넣을 함수 내보내기
export const selectCounter = (state: AppState) => state.counter;
// actions 내보내기
export const { increment, decrement } = counterSlice.actions;
// reducer 내보내기
export default counterSlice.reducer;
  • createSlice: action과 reducer를 한 번에 정의한다.
    • 비동기적인 리듀서 함수를 정의하고자 할 땐 객체의 프로퍼티로 extraReducers 객체를 추가한다.

modules/index.ts

import { combineReducers } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';

import counterReducer from './counterSlice';

const reducer = (state, action) => {
  // SSR 작업 수행 시 필요한 코드
  if (action.type === HYDRATE) {
    return {
      ...state,
      ...action.payload,
    };
  }
  return combineReducers({
    counter: counterReducer,
  })(state, action);
};

export default reducer;
  • modules 내에서 정의한 리덕스 모듈들을 합쳐주는 역할을 한다.
  • if(action.type === HYDRATE)는 SSR 작업 수행 시 HYDRATE라는 액션을 통해서 서버의 스토어와 클라이언트의 스토어를 합쳐주는 작업을 수행한다.
    • Next.js는 처음 렌더링 시 SSR을 하게 된다. 따라서 store를 호출할 때마다 redux store를 새로 생성하게 된다. 이 때 생성하는 redux store와 이후 CSR시 생성하는 redux store가 다르기 때문에 이 둘을 합쳐주는 로직이 필요하다.
    • 서버에서 생성한 store의 상태를 HYDRATE라는 액션을 통해 client에 합쳐주는 작업을 한다. action.payload에는 서버에서 생성한 store의 상태가 담겨있어 이 둘을 합쳐 새로운 client의 redux store의 상태를 만들게 된다.
  • combineReducers: 리듀서 모듈들을 결합하는 역할을 한다.

modules/store.ts

import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import logger from 'redux-logger';

import reducer from './index';

const isDev = process.env.NODE_ENV === 'development';

const makeStore = () =>
  configureStore({
    reducer,
    middleware: getDefaultMiddleware =>
      isDev ? getDefaultMiddleware().concat(logger) : getDefaultMiddleware(),
    devTools: isDev,
  });

export const wrapper = createWrapper(makeStore, {
  debug: isDev,
});

const store = makeStore();

// type 정의
export type AppState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;
  • configureStore: store를 생성한다.
    • redux-toolkit은 devTools 등의 미들웨어들을 기본적으로 제공한다. 따라서 사용하고 싶은 미들웨어가 있다면 추가로 정의할 수 있고, 위의 코드에선 logger를 추가했다.
  • creatWrapper: wrapper를 생성하여 스토어를 바인딩 한다.

pages/_app.tsx

import type { AppProps } from 'next/app';

import { wrapper } from '../modules/store';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default wrapper.withRedux(MyApp);
  • wrapper의 withRedux HOC로 App 컴포넌트를 감싸준다.
  • next-redux-wrapper가 없다면 SSR로 리덕스 스토어에 접근할 수 없다.
    • 각 페이지에서 getStaticProps, getServerSideProps 등의 함수 내에서 스토어 접근이 가능해진다.

동작 화면




혹시 내용에 오류가 있다면 피드백 부탁드립니다. 🙇🏻‍♀️

[참고]

좋은 웹페이지 즐겨찾기