반응형

 

지적되어온 Redux의 문제점들

 

리덕스(Redux)는 리액트(React)의 상태 관리 라이브러리 중 대표로서 굳게 자리매김하고 있었지만, 그동안 리덕스가 사용하기 너무 복잡하고 배우기 어렵다는 의견이 많았습니다. 실제로 리액트에 입문하는 사람들이 리액트를 배우다가 가장 좌절을 겪는 구간이 리덕스를 배울 때이기도 합니다. 리덕스가 높은 러닝 커브와 복잡성을 가지는 원인들을 리덕스 측에서 문서로 공식적으로 발표한 것이 있습니다.

 

 

1. store configuration 복잡성

 

우선, 리덕스는 스토어(store)를 설정하는 게 너무 복잡하다는 것입니다. 리덕스는 앱 전체에 단 하나의 스토어를 가지며 이 스토어는 리액트 앱에서 사용할 상태(state)들을 저장하고 관리합니다. 따라서 스토어는 상태 값들을 사용하고 관리하는 모든 API를 가지며 이것을 설정해주는 것은 복잡한 작업이었다는 것입니다.

 

그러나 저는 이것은 크게 어려운 작업은 아닌 것 같고 오히려 다음 2,3번이 큰 내용이라고 봅니다.

 

 

2. redux를 사용하기 위해선 다른 라이브러리들의 추가설치가 너무 많이 필요했다.

 

정말 그랬습니다. redux를 잘 사용하려면 다음과 같은 패키지들을 추가 설치해줘야 합니다.

 

- react와 바인딩하기 위한 react-redux

- 불변성을 유지하기위한 immutable/immer

- 비동기 통신을 위한 redux-thunk/redux-saga/redux-pender

- 액션 생성을 위한 redux-actions

 

심지어 리액트와 리덕스 특유의 너무 높은 자유도로 인해 프로젝트마다, 개발자 취향마다 설치하는 패키지들이 다 제각각일 수 있습니다.

 

따라서 A패키지에 대해 학습을 하여 프로젝트를 진행했다가, 다른 프로젝트로 옮겨가서는 A 패키지가 아닌 B 패키지를 학습해야 하는 일이 빈번하게 생기는 것입니다.

 

게다가 리액트 특유의 빠르게 변화하는 생태계는 이런 지속적인 학습부담과 피로도를 더욱 가중시킵니다.

 

 

3. 너무 많은 양의 보일러플레이트를 파일마다 반복 작성해야 했다.

 

리덕스 액션과 리듀서들을 도메인마다 분리를 하고, 또 리액트 각 컴포넌트마다 바인딩을 하면, 엄청나게 많은 수의 파일에서 리덕스 코드를 사용하게 됩니다. 그런데 그 많은 파일에서 많은 양의 보일러 플레이트 코드를 반복해서 사용해줘야 해서 코드가 지저분해지고 관리포인트가 많다는 단점이 있습니다.

 

심지어 2번에서 언급했던 추가 패키지들을 뭘 설치하냐에 따라 보일러플레이트 코드가 천차만별이어서 처음 프로젝트를 시작했을 때, 딱 정형화된 보일러 플레이트를 바로 작성하기도 어렵습니다.

 

게다가 리액트 생태계 특유의 빠른 발전 속도 때문에 보일러플레이트를 작성하고 뒤돌아서면 바로 레거시가 되어버리는 점도 개발과 유지보수의 피로도를 가중시킵니다.

 

 

이런 원인들로 인해 리덕스는 너무 복잡하고 배우기 어렵다는 것이 사실 틀린 말은 아닙니다.

 

 

 

 

 

 

문제점들을 날려버린 Redux Toolkit

 

리덕스 툴킷(Redux Toolkit)은 위에서 언급한 단점들을 제거한 리덕스에서 공식적으로 내놓은 패키지입니다.

리덕스 공식 문서도 리덕스를 사용할거면 이 리덕스 툴킷을 사용하는 것이 좋다고 적극 권장하고 있습니다.

위에서 말한 문제점들을 어떤 식으로 해결했는지 하나씩 살펴봅시다.

 

 

1. store configuration

 

먼저 스토어를 생성은 configureStore함수를 실행하여 할 수 있습니다. 이 안에서 리듀서(reducer)들을 바인딩해주면 됩니다.

 

import { configureStore } from "@reduxjs/toolkit"

export const store = configureStore({
  reducer: {},
});

 

그리고 리액트 앱에서는 이 store를 임포트해서 Provider로 제공해줍니다.

 

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

 

 

2. 너무 많은 패키지들 추가 설치

 

리덕스 툴킷은 딱 두 가지 패키지만 설치해서 사용할 수 있습니다.

 

yarn add @reduxjs/toolkit react-redux

 

리덕스 툴킷은 내부적으로 불변성을 지키기 위해 immer라이브러리를 사용하고 비동기 통신을 위해 redux-thunk를 사용합니다. 리덕스 툴킷은 이렇게 내부적으로 의존 라이브러리들을 사용해서 개발자는 툴킷만 설치하면 다른 패키지를 뭘 설치할지 고민할 필요가 없도록 만들었습니다. 그러므로 리덕스 툴킷을 사용할거면 추가 라이브러리 설치를 하지 말고 공식문서에서 가이드하는 대로 딱 저 두 가지만 설치를 해서 사용하는 것이 패키지 사용 통일성을 지킬 수 있어서 좋습니다.

 

 

3. 너무 많은 양의 보일러 플레이트

 

리덕스 툴킷은 보일러 플레이트 코드량을 최소화하도록 노력한 것이 보입니다.

flux패턴 중에 액션 생성 함수, 리듀서 코드는 다음과 같이 작성할 수 있습니다.

예시 코드는 typescript를 적용했습니다.

 

* type.d.ts

type ToDo = {
  id: number;
  content: string;
  checked: boolean;
};

type ToDoList = ToDo[] | [];

interface IToDoListState {
  toDoList: IToDoList;
}

interface IAction {
  type?: string;
}

interface IToggleToDoPayload {
  id: number;
  checked: boolean;
}

interface IToggleToDoAction extends IAction {
  payload: IToggleToDoPayload;
}

interface IDeleteToDoPayload {
  id: number;
}

interface IDeleteToDoAction extends IAction {
  payload: IDeleteToDoPayload;
}

 

* src/store/toDoList/index.ts

import { createAction, createReducer } from "@reduxjs/toolkit";

export const action = {
  toggleToDo: createAction<IToggleToDoPayload>("TOGGLE/TO_DO"),
  deleteToDo: createAction<IDeleteToDoPayload>("DELETE/TO_DO"),
};

const initialState: IToDoListState = {
  toDoList: [],
};

export const reducer = {
  toggleToDo: (state: IToDoListState, action: IToggleToDoAction) => {
    state.toDoList.find((todo: ToDo) => todo.id === action.payload.id).checked = action.payload.checked;
  },
  deleteToDo: (state: IToDoListState, action: IDeleteToDoAction) => {
    state.toDoList = state.toDoList.filter((todo: ToDo) => todo.id !== action.payload.id);
  },
};

const toDoListReducer = createReducer(initialState, builder => {
  builder
    .addCase(action.toggleToDo, reducer.toggleToDo)
    .addCase(action.deleteToDo, reducer.deleteToDo);
});

export default toDoListReducer;

 

이렇게 리듀서 툴킷에서 제공하는 createAction, createReducer만 있으면 리덕스를 사용할 수 있습니다.

빌더 패턴을 사용하여 리듀서 함수들을 추가하기 때문에 createReducer함수를 사용하는 코드가 매우 간결합니다.

 

또 좋은 점은 리듀서는 내부적으로 immer라이브러리를 사용하기 때문에 불변성에 대해서 신경 쓰지 않고 state를 변경시켜주어도 내부적으로 불변성을 지켜줍니다.

 

만든 리듀서는 스토어에 다음과 같이 추가해줍니다.

 

* src/store/index.ts

import { configureStore } from "@reduxjs/toolkit";

import toDoListReducer from "src/store/toDoList";

export const store = configureStore({
  reducer: {
    toDoList: toDoListReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

 

리덕스 코드가 간결한건 알겠고, 그렇다면 리액트랑 바인딩할 때에는 보일러 플레이트가 얼마나 단순해졌는지 볼까요?

List와 Item이라는 컴포넌트를 만들어 살펴보겠습니다.

 

* src/components/toDoList/List.tsx

import { useSelector } from "react-redux";

import Item from "src/components/toDoList/item";
import { RootState } from "src/store";

function List() {
  const toDoList: ToDoList = useSelector((state: RootState) => state.toDoList.toDoList);

  return (
    <div>
      {toDoList && toDoList.map(toDo => <Item key={toDo.id} toDo={toDo} />)}
    </div>
  );
}

export default List;

 

store에 있는 state를 가져오기 위해서는 react-redux가 제공하는 useSelect를 사용합니다. 이때 타입스크립트를 사용한다면, store에서 만든 RootState 타입을 사용해줘야 합니다.

 

* src/components/toDoList/Item.tsx

import { useDispatch } from "react-redux";

import { action } from "src/store/toDoList";

interface IProps {
  toDo: ToDo;
}

function Item({ toDo }: IProps) {
  const { id, content, checked } = toDo;
  const dispatch = useDispatch();

  const onToggle = (event: any) => {
    dispatch(action.toggleToDo({ id, checked: event.target.checked as boolean }));
  };

  const onDeleteToDo = () => {
    dispatch(action.deleteToDo({ id }));
  };

  return (
    <div>
      <div>{id}</div>
      <p>{content}</p>
      <input type="checkbox" defaultChecked={checked} onChange={onToggle} />
      <button onClick={onDeleteToDo}>삭제</Button>
    </div>
  );
}

export default Item;

 

액션을 디스패치(dispatch) 하기 위해서는 react-redux에서 제공하는 useDispatch를 사용합니다. 위 Item 컴포넌트에서는 input check값이 변경되었을 때, 삭제 버튼을 눌렀을 때 각각 이벤트 콜백 함수 안에서 dispatch를 실행하도록 했습니다.

 

컴포넌트 단에서도 useSelect와 useDispatch를 이용하면 보일러 플레이트가 많이 간결하다는 것을 보았습니다.

 

그리고 여기까지 해서 get store state -> dispatch(action) -> run reducer -> update store with new state -> view re-rendering 흐름의 flux 패턴이 구현이 모두 끝났습니다.

 

 

 

마무리 

 

리덕스 툴킷은 이렇게 리덕스의 기존 단점들을 날려버리고 간결하고 정형화된 코드 패턴을 사용할 수 있는 패키지입니다.

리덕스 툴킷에서 제시하는 정형화된 코드 패턴을 사용하면 개발자들 사이의 커뮤니케이션과 협업 난이도도 낮아지므로 리덕스를 사용할거면 이 툴킷을 사용하는 것이 매우 좋다고 생각이 듭니다.

 

이 포스팅 작성시점 기준 패키지버전 @reduxjs/toolkit v1.6.2 react-redux v7.2.6 입니다.

 

 

 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기