Redux-toolkit: 카운터 만들기

jonyChoiGenius·2023년 1월 22일
0

상태관리 툴 언제 쓸까?

Redux는 상태관리 툴이다.

리액트는 props-drilling이 많이 일어난다.
특히 하나의 상태를 관리하기 위해 가장 최상위의 Root컴포넌트에 상태와 함수들을 모아두면 Root컴포넌트는 점점 비대해지고, Drilling되는 Props는 점점 많아지고, 함수의 가독성이 기하급수적으로 떨어지게 된다.

이를 위해 상태관리를 사용하게 되는데,
내가 만들 앱이 큰 규모의 앱은 아니지만 아래의 경우에 유용하다고 생각하였다.

  1. 유저 정보를 관리하기 좋다 : 최초 페이지에서 로그인과 함께 유저 정보를 Fetch해온다. 로그인이 필수인 앱에서는 유저 정보나 로그인 여부를 지속적으로 판단하게 되는데, 이럴 때 상태관리 툴을 사용하면 좋다.
  2. SSR의 성능을 높일 수 있다 : SSR을 하면서 렌더링에 필요한 props를 Fetch해온다. 이때 부모 컴포넌트에서 미리 데이터를 Fetch하여 상태에 저장해두면, 자식 컴포넌트는 추가적인 Fetch 리퀘스트 없이 상태를 조회하기만 하면 된다.

이에 따라 상태관리 툴을 도입하기로 하였는데...
Vue2에서 사용하였던 VUEX만큼 직관적인 상태관리 툴이 없어서 고민이었다.

어떤 상태관리 툴을 쓸까?

리액트에서 자주 쓰이는 상태관리 툴은 크게 4가지였다.

  1. Context API
  2. Redux
  3. Mobx
  4. Recoil
  1. Context API

Context API는 비교적 직관적이었다.
컨텍스트를 선언하고,
사용할 컴포넌트에서 주입하고,
useReducer를 이용해 상태관리를 하면 됐다.

하지만 실제 사용예시 코드들을 보니 점점 복잡해져갔는데
그 이유는 context를 여러개로 쪼개어 관리하는 점(context를 하나로 합치면 해당 context를 주입받는 모든 컴포넌트가 리렌더링 된다고 한다.), 각 컴포넌트 별로 context를 주입받고 reduce한다는 점으로 인해 상태의 추적이 어렵다는 점 등이 문제로 보였다.

  1. Redux

Redux는 가장 많이 쓰이기 때문에 가장 배워보고 싶고 써보고 싶은 툴이었다.
하지만 생각보다 진입 장벽이 높았다.
리덕스를 배우면서 정리했던 글에서 코드를 따라 치면서 열심히 따라갔지만, 코드량이 너무 많았다.
관리할 상태가 하나 늘어날 때마다, initialState를 위한 코드가 3줄씩 증가했고, reducer내부에는 Switch-case문이 기하급수적으로 증가하고...
여기에 state의 불변성도 유지해야 하고(필요하면 immer를 설치해야 하고), 비동기 처리를 위해서 redux-thunk와 같은 미들웨어를 설치해서 숙지하고...
action 함수들을 새로 만들어주어야 하고, 함수들의 변수 헷갈리지 않으려면 index해서 관리해야 하고 등등등.
생각보다 분량이 너무 많아서 손에 익기 전에 머릿속에서 다 뱉어내 버렸다.

3.MobX
검색하다보니 꽤나 많이 나와서 공부해보려고 했는데,
막상 공부 자료가 별로 없었다.
찾아보니 대부분 '리덕스보다 기능도 많고 간결하다', '객체지향이라 요즘 리액트에는 잘 안맞는다' 정도로 정리되는 것 같다. 강의 자료들이 대부분 2-3년 전에 멈추어 있는 이유도 함수형 프로그래밍이 대세가 된 최신 리액트와 잘 맞지 않아서인 듯 하다.

  1. Recoil

Recoil은 Vuex와 가장 흡사하고, 코드도 간결했다. 사람들의 평가를 요약하면 '가장 리액트 다운 상태관리 툴'이었다.
한가지 마음에 걸리는건 'Atoms', 'Selectors'와 같은 기존과는 다른 단어를 선택했다는 점..? 데이터를 원자단위에서 하위 컴포넌트를 거처 상위 컴포넌트로 가는 상향식의 패턴이기 때문이라고 한다. 그다지 와닿지 않았다.(어짜피 FLUX는 State가 계속 돌고 돌잖아?)
그럼에도 Jotai, Zustand 등과 함께 지속적으로 언급되고 비교되는 것을 보면, Recoil이 잘 만들어진 차세대 상태관리 툴임을 의미하는 듯 했다.

갑자기 분위기 Redux-toolkit

그러다가 Redux-toolkit을 발견했다.
그리고 호기심 삼아서 소개 강좌를 들었는데...
쉬웠다.

이미 Redux를 공부하다 포기한 경험이 있어서인듯 했다.

Redux를 공부하며 개념을 알고 있었기 때문에 Redux-toolkit에서 추상화된 내용들이 한 번에 이해가 갔다.
한편 Redux를 공부하다가 포기하게 만들었던 복잡성들이 Redux-toolkit에서는 추상화되면서 단순해졌다.

그래서 Redux-toolkit을 공부하고 도입하기로 하였다.
Reudx-toolkit의 좋은 점은
1) 기존 Redux와 호환된다는 점
2) Ducks패턴을 굳이 만들지 않아도 Slice로 관심사를 분리하여 관리할 수 있다는 점
3) 불변성을 지켜줌!
4) 함수를 Dispatch할 수 있음!
5) 자동으로 Actions함수 만들어 줌!

여러모로 Redux-toolkit만으로도 추상화된 Redux 보일러 플레이트를 얻는 기분이다.
Redux 공식문서에서도 권장하는 방법이 된 만큼 지속적인 지원이 이루어질 것으로 보인다.

무엇보다 Vuex에서 익숙해진 Action-Dispatch-Store라는 친숙한 패러다임을 그대로 쓸 수 있다는 점이 좋았다.

Redux와 Redux-toolkit 샘플 코드 비교

Redux

지난번 작성한 Next js 환경에서 테스트 해보자

예제 코드는 생활 코딩님의 Redux Toolkit 강좌를 퍼왔다.

_App.tsx에서 작성한 코드이다.

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

import { createWrapper } from "next-redux-wrapper";
import { createStore } from "@reduxjs/toolkit";

// (2)
function reducer(state, action) {
  if (action.type === "up") {
    return { ...state, value: state.value + action.step };
  }
  return state;
}

// (1)
const initialState = { value: 0 };

// (3)
const makeStore = () => {
  const store = createStore(reducer, initialState);
  return store;
};

// (4)
const wrapper = createWrapper(makeStore, {
  debug: process.env.NODE_ENV === "development",
});

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

// (5)
export default wrapper.withRedux(App);
  1. state를 객체로 초기화 해준다.
    state 내부에 value라는 상태가 0으로 초기화되어있다.
  2. reducer 함수를 만들어 준다.
    reduter는 state와 action을 받는다. action타입이 'up'이면, state를 깊은 복사해서, 새로운 value를 덮어 씌운다.
  3. createStore로 새로운 스토어를 만든다. (next.js에서는 이 부분을 makeStore라는 새로운 함수로 감싸주어야 한다.)
  4. App.js를 Provider로 감싸 컴포넌트들에게 주입한다. (next.js에서는 createWrapper(makeStore)로 만들어진 wrapper로 감싸준다.

이렇게 주입된 store를 react-redux 라이브러리를 이용해 접근할 수 있다.

import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";

function Counter() {
  const dispatch = useDispatch(); // (1)
  const count = useSelector((state) => state.value); //(2)

  return (
    <div>
      {" "}
      {count}
      <button
        onClick={() => {
          dispatch({ type: "up", step: 2 }); //(3)
        }}
      >
        더하기
      </button>
    </div>
  );
}

export default Counter;
  1. useDispatch()로 새로운 Dispatch 함수를 만들어준다. dispatch는 액션을 실행해주는 함수이다.
  2. useSelector()는 state를 조회해 특정 state를 가져오는 Hook이다. state의 value를 가져와 count라는 식별자로 선언했다.
  3. 클릭이 일어날 때에, dispatch에 액션 객체를 넘겨주는데, 이때 타입 값이 필수이다. 'up'으로 지정해주고, step을 작성하면, 위에서 작성한 reducer(state, action)가 실행되며 { ...state, value: state.value + action.step }가 상태에 적용된다.

Redux-Toolkit

위의 redux 예시와 최대한 동일하게 redux-toolkit으로 작성해 보았다.

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

import { createWrapper } from "next-redux-wrapper";
import { configureStore, createSlice } from "@reduxjs/toolkit";

//(1)
export const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    up(state, action) {
      // state.value = state.value + action.step;
      state.value = state.value + action.payload;
    },
  },
});


//(2)
const makeStore = () => {
  const store = configureStore({
    reducer: {
      counterForStore: counterSlice.reducer,
    },
  });
  return store;
};

//(3)
const wrapper = createWrapper(makeStore, {
  debug: process.env.NODE_ENV === "development",
});

function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <ThemeProvider theme={theme}>
        <GlobalStyle />
        <Component {...pageProps} />
      </ThemeProvider>
    </>
  );
}

//(4)
export default wrapper.withRedux(App);

(Next 8 이상에서는 위 코드에서 경고가 난다 useWrappedStore 사용을 권장한다.)

  1. createSlice로 새로운 객체를 만들어준다. 이때 reducers는 reducer함수에 들어갈 메서드들의 집합이다.
  2. configureStore로 새로운 store를 만들어 준다. recuder 안에 각 관심사별 reducer들을 담아준다. counterSlice.reducer는 reducers를 하나로 합친 것이다.(해당 리듀서는 구분을 위해 counterForStore라고 식별해줌.)
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";

//(1)
import { counterSlice } from "./_app";

function Counter() {
  //(2)
  const dispatch = useDispatch();
  //(3)
  const count = useSelector((state) => state.counterForStore.value);

  return (
    <div>
      {" "}
      {count}
      <button
        onClick={() => {
          //(4)
          // dispatch({ type: "counter/up", step: 2 });
          dispatch(counterSlice.actions.up(2));
        }}
      >
        더하기
      </button>
    </div>
  );
}

export default Counter;
  1. counterSlice를 import한다.
  2. 리덕스 툴킷은 actionCreator를 자동으로 해준다. 아래와 같이 actions 내부에 있는 up 액션함수를 실행할 수 있다. 이때 입력한 value는 'payload'라는 값으로 넘어간다.

비교

redux와 redux-toolkit을 비교해보면,
전체적인 로직은 같지만 redux-toolkit이 코드량이 크게 줄고 모듈화가 편해진 것을 확인할 수 있다.
1. ducks타입으로 코딩하여 각 파일별로 initialState를 만들고, reducers에 타입별로 함수를 선언하여 return하던 것을 slice라는 하나의 객체로 추상화한다. 이에 따라 store를 선언할 때에도 각 slice의 reducer만 받으면 된다.
2. dispatch할 때에 일일이 type과 프로퍼티명을 선언하거나 actionCreator로 액션함수를 만들던 것을 slice에 접근하여 액션 함수를 실행시키는 것으로 단순화했다. 이때 액션함수에 넘겨주는 인자는 payload라는 파라미터로 받는다.

redux-toolkit은 리덕스의 강력함은 그대로 유지한 채, 불필요하게 반복해야 했던 작업들은 추상화하여 매우 편리하고 보다 직관적인 로직에 집중할 수 있다.

profile
천재가 되어버린 박제를 아시오?

0개의 댓글