소프트웨어 엔지니어링/프론트 엔드

상태관리 라이브러리 비교

dhsimpson 2023. 10. 15. 23:53

상태관리 라이브러리 비교

각 라이브러리 사용성을 비교하기 위해 하나의 예시를 가지고 라이브러리 마다의 코드를 비교해 봤다.

 

예시 비즈니스 로직 :

옷을 판매하는 서비스를 운영중이며, 옷 상품을 등록하는 화면의 경우이다.

url은 다음과 같다 :  /regist/clothes/:category

:category - path variable 이며 예시로는 신발(shoes), 상의(top), 하의(pants) 가 있다.

옷 상품 등록 화면의 일부 컴포넌트들은 카테고리 마다 다른 정보를 받도록 화면을 렌더링 해야 한다.

if-else 조건문을 이용한다면 신규 category가 추가됨에 따라 UI 로직이 너무 더럽혀질 것이기에,

map 을 이용해서 category 값에 따라 해당 컴포넌트들을 렌더링 하려 한다.

 

ex)

const categoryMap = {
	shoes: {
    	registData1: ShoesData1, 
        registData2: ShoesData2, 
    }, 
    top: {
    	registData1: TopData1, 
        registData2: TopData2, 
        }, 
    pants: {
    	registData1: PantsData1, 
        registData2: PantsData2, 
        }
}

 

이러한 map 방식을 이용할 때 상태관리 라이브러리를 이용할 것이다.

 

Redux, Recoil, Mobx, Zustand 라이브러리를 사용한 예시들을 비교해 보자.

 

Redux

더보기

1. 액션 타입, 액션 생성자, 리듀서 정의:

// actionTypes.js
export const CHANGE_CATEGORY = 'CHANGE_CATEGORY';

// actions.js
import { CHANGE_CATEGORY } from './actionTypes';

export const changeCategory = (category) => ({
  type: CHANGE_CATEGORY,
  payload: category
});

// reducer.js
import { CHANGE_CATEGORY } from './actionTypes';

const initialState = {
  selectedCategory: 'shoes'
};

const clothesReducer = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_CATEGORY:
      return {
        ...state,
        selectedCategory: action.payload
      };
    default:
      return state;
  }
};

export default clothesReducer;

2. 스토어 생성:

// store.js
import { createStore } from 'redux';
import clothesReducer from './reducer';

const store = createStore(clothesReducer);

export default store;

3. Provider를 사용하여 앱에 스토어 연결:

// App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import ClothesRegist from './ClothesRegist';

function App() {
  return (
    <Provider store={store}>
      <ClothesRegist />
    </Provider>
  );
}

export default App;

4. 컴포넌트에서 redux 상태와 액션 사용:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeCategory } from './actions';

const categoryMap = {
  shoes: { registData1: ShoesData1, registData2: ShoesData2 },
  top: { registData1: TopData1, registData2: TopData2 },
  pants: { registData1: PantsData1, registData2: PantsData2 },
};

function ClothesRegist() {
  const selectedCategory = useSelector((state) => state.selectedCategory);
  const dispatch = useDispatch();

  const componentsToRender = categoryMap[selectedCategory];

  return (
    <div>
      <h1>{selectedCategory} 등록</h1>

      <div>
        <button onClick={() => dispatch(changeCategory('shoes'))}>신발</button>
        <button onClick={() => dispatch(changeCategory('top'))}>상의</button>
        <button onClick={() => dispatch(changeCategory('pants'))}>하의</button>
      </div>

      {Object.values(componentsToRender).map((Component, index) => (
        <Component key={index} />
      ))}
    </div>
  );
}

export default ClothesRegist;

 

Recoil

더보기

1. Recoil 상태 설정:

import { atom } from 'recoil';

export const selectedCategoryState = atom({
  key: 'selectedCategoryState',
  default: 'shoes', // 기본값으로 'shoes' 설정
});

2. 카테고리에 따른 컴포넌트 매핑:

const categoryMap = {
  shoes: { registData1: ShoesData1, registData2: ShoesData2 },
  top: { registData1: TopData1, registData2: TopData2 },
  pants: { registData1: PantsData1, registData2: PantsData2 },
};

3. Recoil Root 등록

import React from 'react';
import { RecoilRoot } from 'recoil';
import ClothesRegist from './ClothesRegist'; // ClothesRegist 컴포넌트를 가져옵니다.

function App() {
  return (
    <RecoilRoot>
      <div className="App">
        {/* 다른 컴포넌트들 */}
        <ClothesRegist />
        {/* 다른 컴포넌트들 */}
      </div>
    </RecoilRoot>
  );
}

export default App;

4. 컴포넌트 렌더링:

function ClothesRegist() {
  const [selectedCategory, setSelectedCategory] = useRecoilState(selectedCategoryState);
  const componentsToRender = categoryMap[selectedCategory];
import { useRecoilState } from 'recoil';
import { selectedCategoryState } from './pathToYourAtom';


function changeCategory(category) {
    setSelectedCategory(category);
  }

  return (
    <div>
      <h1>{selectedCategory} 등록</h1>

      {/* 카테고리 변경 버튼 */}
      <div>
        <button onClick={() => changeCategory('shoes')}>신발</button>
        <button onClick={() => changeCategory('top')}>상의</button>
        <button onClick={() => changeCategory('pants')}>하의</button>
      </div>

      {Object.values(componentsToRender).map((Component, index) => (
        <Component key={index} />
      ))}
    </div>
  );
}

Mobx

더보기

1. MobX 상태 설정:

import { observable, action } from 'mobx';

class ClothesStore {
  @observable selectedCategory = 'shoes';

  @action
  changeCategory(category) {
    this.selectedCategory = category;
  }
}

const clothesStore = new ClothesStore();
export default clothesStore;

2. 카테고리에 따른 컴포넌트 매핑:

const categoryMap = {
  shoes: { registData1: ShoesData1, registData2: ShoesData2 },
  top: { registData1: TopData1, registData2: TopData2 },
  pants: { registData1: PantsData1, registData2: PantsData2 },
};

3. 컴포넌트 렌더링:

import { observer } from 'mobx-react';
import clothesStore from './pathToYourStore';

const ClothesRegist = observer(() => {
  const componentsToRender = categoryMap[clothesStore.selectedCategory];

  return (
    <div>
      <h1>{clothesStore.selectedCategory} 등록</h1>

      {/* 카테고리 변경 버튼 */}
      <div>
        <button onClick={() => clothesStore.changeCategory('shoes')}>신발</button>
        <button onClick={() => clothesStore.changeCategory('top')}>상의</button>
        <button onClick={() => clothesStore.changeCategory('pants')}>하의</button>
      </div>

      {Object.values(componentsToRender).map((Component, index) => (
        <Component key={index} />
      ))}
    </div>
  );
});

export default ClothesRegist;

 

Zustand

더보기

1. Zustand 상태 설정:

import create from 'zustand';

const useClothesStore = create((set) => ({
  selectedCategory: 'shoes',
  changeCategory: (category) => set({ selectedCategory: category }),
}));

export default useClothesStore;

2. 카테고리에 따른 컴포넌트 매핑:

const categoryMap = {
  shoes: { registData1: ShoesData1, registData2: ShoesData2 },
  top: { registData1: TopData1, registData2: TopData2 },
  pants: { registData1: PantsData1, registData2: PantsData2 },
};

3. 컴포넌트 렌더링

import useClothesStore from './pathToYourStore';

function ClothesRegist() {
  const selectedCategory = useClothesStore((state) => state.selectedCategory);
  const changeCategory = useClothesStore((state) => state.changeCategory);
  const componentsToRender = categoryMap[selectedCategory];

  return (
    <div>
      <h1>{selectedCategory} 등록</h1>

      {/* 카테고리 변경 버튼 */}
      <div>
        <button onClick={() => changeCategory('shoes')}>신발</button>
        <button onClick={() => changeCategory('top')}>상의</button>
        <button onClick={() => changeCategory('pants')}>하의</button>
      </div>

      {Object.values(componentsToRender).map((Component, index) => (
        <Component key={index} />
      ))}
    </div>
  );
}

export default ClothesRegist;

 

결론

 - redux : 무조건 Provider 로 app.js 를 wrap 해야 사용 가능함.
recoil : 무조건 recoilRoot(==provider) 로 app.js 를 wrap 해야 사용 가능함. 
state get, set 을 하려면 useRecoilState 와 recoil Atom 을 한번에 다 import 해야만 하는 번거로움 존재
비즈니스 로직이 매우매우매우 복잡한 경우(depth 가 많거나 state 끼리의 의존성이 복잡해지는 경우) 적합.
mobx : js 언어 사용 방식에 일관성을 깨뜨림 (reactjs 에선 functional 위주인데, mobx는 class & annotation 이 필수) 
context-api : 사용법이 너무 복잡함 (context api 생성, provider 생성, provider 에 직접 state, getter, setter 등을 전달해줘야 함 - 전역으로 관리 할 state 가 많아질 수록 코드량이 너무 많이 늘어남)  

코드량 상승을 야기하는 context-api, js 언어 사용 방식의 일관성을 깨뜨리는 mobx 를 제외한다면 redux, recoil, zustand 가 남음. 

이 중, zustand 이 가장 사용법이 간단하며 provider 로 app.js 를 wrap 할 필요도 없어 사용하기 가장 적합해 보임 (zustand 이 기존 라이브러리들의 '기본적으로 필요한 것들' 만 가져오고 provider 처럼 필수인 요소는 개발자가 몰라도 되도록 해주는 것으로 보임, get, set 을 할 때에도 get set 하는 비즈니스 로직들을 전부 custom hook 형태로 만들 수 있어 recoil 에서의 불편함을 해소해 줌)