Redux는 상태관리 툴이다.
리액트는 props-drilling이 많이 일어난다.
특히 하나의 상태를 관리하기 위해 가장 최상위의 Root컴포넌트에 상태와 함수들을 모아두면 Root컴포넌트는 점점 비대해지고, Drilling되는 Props는 점점 많아지고, 함수의 가독성이 기하급수적으로 떨어지게 된다.
이를 위해 상태관리를 사용하게 되는데,
내가 만들 앱이 큰 규모의 앱은 아니지만 아래의 경우에 유용하다고 생각하였다.
이에 따라 상태관리 툴을 도입하기로 하였는데...
Vue2에서 사용하였던 VUEX만큼 직관적인 상태관리 툴이 없어서 고민이었다.
리액트에서 자주 쓰이는 상태관리 툴은 크게 4가지였다.
Context API는 비교적 직관적이었다.
컨텍스트를 선언하고,
사용할 컴포넌트에서 주입하고,
useReducer를 이용해 상태관리를 하면 됐다.
하지만 실제 사용예시 코드들을 보니 점점 복잡해져갔는데
그 이유는 context를 여러개로 쪼개어 관리하는 점(context를 하나로 합치면 해당 context를 주입받는 모든 컴포넌트가 리렌더링 된다고 한다.), 각 컴포넌트 별로 context를 주입받고 reduce한다는 점으로 인해 상태의 추적이 어렵다는 점 등이 문제로 보였다.
Redux는 가장 많이 쓰이기 때문에 가장 배워보고 싶고 써보고 싶은 툴이었다.
하지만 생각보다 진입 장벽이 높았다.
리덕스를 배우면서 정리했던 글에서 코드를 따라 치면서 열심히 따라갔지만, 코드량이 너무 많았다.
관리할 상태가 하나 늘어날 때마다, initialState를 위한 코드가 3줄씩 증가했고, reducer내부에는 Switch-case문이 기하급수적으로 증가하고...
여기에 state의 불변성도 유지해야 하고(필요하면 immer를 설치해야 하고), 비동기 처리를 위해서 redux-thunk와 같은 미들웨어를 설치해서 숙지하고...
action 함수들을 새로 만들어주어야 하고, 함수들의 변수 헷갈리지 않으려면 index해서 관리해야 하고 등등등.
생각보다 분량이 너무 많아서 손에 익기 전에 머릿속에서 다 뱉어내 버렸다.
3.MobX
검색하다보니 꽤나 많이 나와서 공부해보려고 했는데,
막상 공부 자료가 별로 없었다.
찾아보니 대부분 '리덕스보다 기능도 많고 간결하다', '객체지향이라 요즘 리액트에는 잘 안맞는다' 정도로 정리되는 것 같다. 강의 자료들이 대부분 2-3년 전에 멈추어 있는 이유도 함수형 프로그래밍이 대세가 된 최신 리액트와 잘 맞지 않아서인 듯 하다.
Recoil은 Vuex와 가장 흡사하고, 코드도 간결했다. 사람들의 평가를 요약하면 '가장 리액트 다운 상태관리 툴'이었다.
한가지 마음에 걸리는건 'Atoms', 'Selectors'와 같은 기존과는 다른 단어를 선택했다는 점..? 데이터를 원자단위에서 하위 컴포넌트를 거처 상위 컴포넌트로 가는 상향식의 패턴이기 때문이라고 한다. 그다지 와닿지 않았다.(어짜피 FLUX는 State가 계속 돌고 돌잖아?)
그럼에도 Jotai, Zustand 등과 함께 지속적으로 언급되고 비교되는 것을 보면, Recoil이 잘 만들어진 차세대 상태관리 툴임을 의미하는 듯 했다.
그러다가 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라는 친숙한 패러다임을 그대로 쓸 수 있다는 점이 좋았다.
지난번 작성한 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);
이렇게 주입된 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;
위의 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 사용을 권장한다.)
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;
redux와 redux-toolkit을 비교해보면,
전체적인 로직은 같지만 redux-toolkit이 코드량이 크게 줄고 모듈화가 편해진 것을 확인할 수 있다.
1. ducks타입으로 코딩하여 각 파일별로 initialState를 만들고, reducers에 타입별로 함수를 선언하여 return하던 것을 slice라는 하나의 객체로 추상화한다. 이에 따라 store를 선언할 때에도 각 slice의 reducer만 받으면 된다.
2. dispatch할 때에 일일이 type과 프로퍼티명을 선언하거나 actionCreator로 액션함수를 만들던 것을 slice에 접근하여 액션 함수를 실행시키는 것으로 단순화했다. 이때 액션함수에 넘겨주는 인자는 payload라는 파라미터로 받는다.
redux-toolkit은 리덕스의 강력함은 그대로 유지한 채, 불필요하게 반복해야 했던 작업들은 추상화하여 매우 편리하고 보다 직관적인 로직에 집중할 수 있다.