상태관리 라이브러리 비교
각 라이브러리 사용성을 비교하기 위해 하나의 예시를 가지고 라이브러리 마다의 코드를 비교해 봤다.
예시 비즈니스 로직 :
옷을 판매하는 서비스를 운영중이며, 옷 상품을 등록하는 화면의 경우이다.
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 에서의 불편함을 해소해 줌)
'소프트웨어 엔지니어링 > 프론트 엔드' 카테고리의 다른 글
Axios 사용시 예외처리에서 주의할 점 (0) | 2023.06.16 |
---|---|
react-query : cache, refetch, interval, observer (0) | 2023.03.04 |
vscode 에서 react 프로젝트 새로 셋팅하기 (0) | 2023.02.03 |
[원티드1월챌린지] FE 프리온보딩(React-Query) 종료후 과제 보충 (0) | 2023.01.25 |
[원티드1월챌린지] FE 프리온보딩(React-Query) 후기 (0) | 2023.01.20 |