전역 상태관리
전역상태관리
전역상태관리 정리 입니다. redux, recoil, zustand 3개의 전역상태 라이브러리를 사용해 봤는데요. 기존 프로젝트가 redux, recoil 이라면 해당 기술을 통해 유지보수를 하면 될테고요. 신규 프로젝트는 zustand 를 권장드립니다.
근데 IT 프로젝트가 생각보다 신규 구축보다 옛날 프로젝트를 운영, 기능확장 하는 비율이 훨씬 높아서요. 고객사랑 레거시 기술을 최신기술로 바꾸는게 협의되고 작업을 하는거랑 기술자가 무턱대고 최신기술을 도입하려고 프로젝트를 뜯어고치는 거랑 차이가 큰데요, 소통이 필수라고 생각해요.
보통 신입 분들이 최신 기술을 많이 배워와서 옛날 프로젝트를 자기만의 방식으로 협의없이 뜯어고치는 경우가 더러 있는데요, 주의가 필요합니다. 좋은 기술 적극적으로 현업에 적용시키는 것은 칭찬받을 일이긴 한데요, 협의가 된 후 적용하는게 필수라고 봐요.
고객사는 생각보다 많이 바꾸지 않고 최선의 효율책을 내는 것을 원하는 경우가 많아서요. 괜히 바꿨다가 잘 되던 것도 안되면 리스크가 크지 않겠습니까?
본인이 실현시키려고 하는 기술을 상대방은 원하지 않을 수도 있어요. 팀, 회사 큰 그림에서 좋은 방향으로 항상 생각하시길 바래요.
결론적으로 융통성 있게 일합시다.
참고로 1,2번 목차는 zustand 공식문서에서 발췌한 내용입니다. 시작하겠습니다.
Zustand vs. Other State Management Libraries
1. Zustand vs. Redux
1.1 State Model (vs Redux)
Zustand and Redux both follow an immutable state model, but Redux requires wrapping the app in context providers, while Zustand does not.
Zustand Example
import { create } from 'zustand'
type State = {
count: number
}
type Actions = {
increment: (qty: number) => void
decrement: (qty: number) => void
}
const useCountStore = create<State & Actions>((set) => ({
count: 0,
increment: (qty: number) => set((state) => ({ count: state.count + qty })),
decrement: (qty: number) => set((state) => ({ count: state.count - qty })),
}))
Alternative Zustand approach using reducers:
import { create } from 'zustand'
type State = {
count: number
}
type Actions = {
increment: (qty: number) => void
decrement: (qty: number) => void
}
type Action = {
type: keyof Actions
qty: number
}
const countReducer = (state: State, action: Action) => {
switch (action.type) {
case 'increment':
return { count: state.count + action.qty }
case 'decrement':
return { count: state.count - action.qty }
default:
return state
}
}
const useCountStore = create<State & Actions>((set) => ({
count: 0,
dispatch: (action: Action) => set((state) => countReducer(state, action)),
}))
Redux Example
import { createStore } from 'redux'
import { useSelector, useDispatch } from 'react-redux'
type State = {
count: number
}
type Action = {
type: 'increment' | 'decrement'
qty: number
}
const countReducer = (state: State, action: Action) => {
switch (action.type) {
case 'increment':
return { count: state.count + action.qty }
case 'decrement':
return { count: state.count - action.qty }
default:
return state
}
}
const countStore = createStore(countReducer)
Using Redux Toolkit:
import { createSlice, configureStore } from '@reduxjs/toolkit'
const countSlice = createSlice({
name: 'count',
initialState: { value: 0 },
reducers: {
incremented: (state, qty: number) => {
state.value += qty
},
decremented: (state, qty: number) => {
state.value -= qty
},
},
})
const countStore = configureStore({ reducer: countSlice.reducer })
1.2 Render Optimization (vs Redux)
Both Zustand and Redux require manually applying render optimizations using selectors.
Zustand Example
import { create } from 'zustand'
type State = {
count: number
}
type Actions = {
increment: (qty: number) => void
decrement: (qty: number) => void
}
const useCountStore = create<State & Actions>((set) => ({
count: 0,
increment: (qty: number) => set((state) => ({ count: state.count + qty })),
decrement: (qty: number) => set((state) => ({ count: state.count - qty })),
}))
const Component = () => {
const count = useCountStore((state) => state.count)
const increment = useCountStore((state) => state.increment)
const decrement = useCountStore((state) => state.decrement)
}
Redux Example
import { useSelector, useDispatch } from 'react-redux'
type State = {
count: number
}
type Action = {
type: 'increment' | 'decrement'
qty: number
}
const Component = () => {
const count = useSelector((state: State) => state.count)
const dispatch = useDispatch()
}
Using Redux Toolkit:
import { useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import { createSlice, configureStore } from '@reduxjs/toolkit'
const countSlice = createSlice({
name: 'count',
initialState: { value: 0 },
reducers: {
incremented: (state, qty: number) => {
state.value += qty
},
decremented: (state, qty: number) => {
state.value -= qty
},
},
})
const countStore = configureStore({ reducer: countSlice.reducer })
const useAppSelector: TypedUseSelectorHook<typeof countStore.getState> =
useSelector
const useAppDispatch: () => typeof countStore.dispatch = useDispatch
const Component = () => {
const count = useAppSelector((state) => state.count.value)
const dispatch = useAppDispatch()
}
2. Zustand vs. Recoil
2.1 State Model (vs Recoil)
Recoil depends on atom string keys, while Zustand does not. Recoil requires wrapping the app in a context provider.
Zustand Example
import { create } from 'zustand'
type State = {
count: number
}
type Actions = {
setCount: (countCallback: (count: State['count']) => State['count']) => void
}
const useCountStore = create<State & Actions>((set) => ({
count: 0,
setCount: (countCallback) =>
set((state) => ({ count: countCallback(state.count) })),
}))
Recoil Example
import { atom } from 'recoil'
const count = atom({
key: 'count',
default: 0,
})
2.2 Render Optimization (vs Recoil)
Recoil makes render optimizations through atom dependency, while Zustand requires manually applying optimizations using selectors.
Zustand Example
const Component = () => {
const count = useCountStore((state) => state.count)
const setCount = useCountStore((state) => state.setCount)
}
Recoil Example
import { atom, useRecoilState } from 'recoil'
const countAtom = atom({
key: 'count',
default: 0,
})
const Component = () => {
const [count, setCount] = useRecoilState(countAtom)
}
3. Zustand란?
Zustand는 React 애플리케이션에서 전역 상태 관리를 간편하게 할 수 있도록 도와주는 라이브러리이다. Redux보다 설정이 간단하며, React의 Context API보다 성능이 뛰어나며 불필요한 렌더링을 최소화할 수 있다.
import create from 'zustand'
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, increase } = useStore()
return (
<div>
<p>Count: {count}</p>
<button onClick={increase}>Increase</button>
</div>
)
}
3.1. Zustand의 주요 특징
Zustand는 다른 상태 관리 라이브러리와 비교하여 몇 가지 차별점을 가진다.
심플한 API: 상태를 정의하는 데 필요한 코드가 적고 간결하다.
불필요한 렌더링 방지: 필요한 컴포넌트만 리렌더링된다.
미들웨어 지원:
persist
,devtools
,immer
등 다양한 기능을 추가할 수 있다.비동기 상태 관리 가능: 상태를
async
함수로 업데이트할 수 있다.예제: 불필요한 렌더링 방지
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
function Display() {
const count = useStore((state) => state.count)
return <p>Count: {count}</p>
}
useStore((state) => state.count);
처럼 특정 상태만 구독하면, 관련된 상태가 변경될 때만 렌더링된다.
3.2. Zustand 기본 사용법
3.2.1. 상태 정의 및 사용
const useStore = create((set) => ({
name: 'John Doe',
setName: (newName) => set({ name: newName }),
}))
function Profile() {
const { name, setName } = useStore()
return (
<div>
<p>Name: {name}</p>
<button onClick={() => setName('Jane Doe')}>Change Name</button>
</div>
)
}
3.2.2. 비동기 상태 관리
const useStore = create((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await response.json()
set({ data })
},
}))
3.3. Zustand 미들웨어 활용
3.3.1. 상태 지속화 (persist
)
import create from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'counter-storage',
},
),
)
persist
미들웨어를 사용하면 상태가localStorage
에 저장되어 새로고침 후에도 유지된다.
3.3.2. Devtools 연동
import create from 'zustand'
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
})),
)
Redux DevTools를 사용하여 상태 변화를 추적할 수 있다.
3.4. Zustand 활용 예제
3.4.1. 전역 상태를 활용한 다크 모드 설정
const useThemeStore = create((set) => ({
darkMode: false,
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
}))
function ThemeToggle() {
const { darkMode, toggleDarkMode } = useThemeStore()
return (
<button onClick={toggleDarkMode}>
{darkMode ? 'Light Mode' : 'Dark Mode'}
</button>
)
}
4. API
4.1 createStore
createStore
는 zustand
의 상태 관리 스토어를 생성하는 함수입니다. 이는 store
를 생성하는 기본적인 방식이며, 객체 형태의 상태를 관리하는 데 사용됩니다.
사용 예시
import { createStore } from 'zustand'
const useStore = createStore((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
console.log(useStore.getState().count) // 0
useStore.setState({ count: 5 })
console.log(useStore.getState().count) // 5
4.2 createWithEqualityFn
createWithEqualityFn
은 기본 createStore
함수와 유사하지만, 상태 변경을 감지할 때 사용자 정의 비교 함수를 사용할 수 있습니다.
사용 예시
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'
const useStore = createWithEqualityFn(
(set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}),
shallow,
)
4.3 create
기본적인 zustand
의 상태 생성 함수로, 기존의 createStore
를 래핑한 형태입니다.
사용 예시
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
4.4 shallow
shallow
는 객체 또는 배열 상태를 비교할 때 얕은 비교를 수행하는 유틸리티입니다. useShallow
또는 createWithEqualityFn
에서 활용됩니다.
사용 예시
import { shallow } from 'zustand/shallow'
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 1, b: 2 }
console.log(shallow(obj1, obj2)) // true
5. Hooks
5.1 useShallow
useShallow
는 zustand
상태를 비교할 때 shallow
비교를 적용하는 hook
입니다.
사용 예시
import { useShallow } from 'zustand/react'
const useStore = create((set) => ({
user: { name: 'Alice', age: 25 },
}))
const user = useStore((state) => state.user, useShallow)
5.2 useStoreWithEqualityFn
useStoreWithEqualityFn
은 상태 선택 시 사용자 정의 비교 함수를 적용할 수 있는 hook
입니다.
사용 예시
import { useStoreWithEqualityFn } from 'zustand/react'
const useStore = create((set) => ({
items: [1, 2, 3],
}))
const items = useStoreWithEqualityFn(
(state) => state.items,
(a, b) => JSON.stringify(a) === JSON.stringify(b),
)
5.3 useStore
useStore
는 zustand
에서 가장 기본적인 hook
으로, 생성된 store
를 활용할 때 사용됩니다.
사용 예시
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const count = useStore((state) => state.count)
const increase = useStore((state) => state.increase)
return (
<div>
<p>Count: {count}</p>
<button onClick={increase}>Increase</button>
</div>
)
}