📎

React 상태관리 라이브러리 선정 가이드(Context API, Zustand, MobX, Valtio)

개요

React 프로젝트에서 Context API, Zustand, Valtio, MobX 중 어떤 상태관리 라이브러리를 선택할지 결정하는 가이드입니다. 각 라이브러리가 해결하는 문제와 한계를 분석하여 프로젝트에 맞는 선택을 도와드립니다.

왜 Redux와 Recoil/Jotai는 제외했나요?

Redux의 경우 보일러플레이트가 많고 학습 곡선이 가파르며, Redux Toolkit을 사용해도 여전히 복잡합니다. 현재는 더 간단하고 효율적인 대안들이 있습니다.
Recoil과 Jotai 같은 Atomic 패턴의 경우 Facebook이 Recoil 개발을 중단했고, Jotai는 학습 비용이 높으면서도 제공하는 이점이 Zustand나 Valtio 대비 명확하지 않습니다.
이 가이드는 2025년 현재 실무에서 가장 널리 사용되고 유지보수가 활발한 라이브러리들을 중심으로 작성되었습니다.

빠른 선택 가이드

프로젝트 특성
권장 라이브러리
주요 이유
소규모, 안정적 상태
Context API
React 내장, 추가 의존성 없음
중규모, 성능 중시
Zustand
선택적 구독, 간단한 API
대규모, 복잡한 상태 관계
Valtio/MobX
자동 반응성, 정교한 상태 관리
팀이 OOP에 익숙
MobX
클래스 기반, 명확한 구조
함수형 패러다임 선호
Valtio
Hooks 패턴, 자연스러운 문법

1️⃣ Context API의 한계

React의 기본 상태관리인 Context API는 props drilling 문제를 해결하지만, 복잡한 애플리케이션에서는 한계가 있습니다.

문제 1: 전체 구독 방식

Context 값의 일부만 변경되어도 모든 구독 컴포넌트가 리렌더링됩니다.
const AppContext = createContext({ user: { name: 'John', email: 'john@example.com' }, posts: [...], ui: { theme: 'dark' } }); // user.email만 변경되어도 모든 컴포넌트가 리렌더링

문제 2: Provider Hell

Context를 세분화하면 Provider가 중첩됩니다.
<UserProvider> <PostsProvider> <UIProvider> <SettingsProvider> <App /> {/* 관리가 어려움 */} </SettingsProvider> </UIProvider> </PostsProvider> </UserProvider>

문제 3: 복잡한 메모이제이션

불필요한 리렌더링을 방지하려면 모든 값을 메모이제이션해야 합니다.
function UserProvider({ children }) { const [user, setUser] = useState({ name: 'John' }); const updateUser = useCallback((updates) => { setUser(prev => ({ ...prev, ...updates })); }, []); const contextValue = useMemo(() => ({ user, updateUser }), [user, updateUser]); return ( <UserContext.Provider value={contextValue}> {children} </UserContext.Provider> ); }

Context API 사용 시기

테마, 언어 설정, 인증 상태처럼 안정적이고 자주 변경되지 않는 전역 상태에 적합합니다.

2️⃣ Zustand

Zustand는 Context API의 주요 문제들을 해결한 경량 상태관리 라이브러리입니다.

해결책 1: Provider 제거

복잡한 Provider 중첩 없이 React 외부 상태를 사용할 수 있습니다.
const useStore = create((set) => ({ user: { name: 'John', age: 30 }, posts: [], updateUser: (updates) => set((state) => ({ user: { ...state.user, ...updates } })) })); function App() { return <HomePage />; // Provider 불필요 } function UserProfile() { const user = useStore(state => state.user); return <h1>{user.name}</h1>; }

해결책 2: 선택적 구독

필요한 상태 부분만 구독하여 불필요한 리렌더링을 방지합니다.
const useAppStore = create((set) => ({ user: { name: 'John', age: 30, email: 'john@example.com' }, posts: { items: [], loading: false }, ui: { theme: 'dark', sidebar: false } })); function UserName() { const userName = useAppStore(state => state.user.name); // posts나 ui 변경시 리렌더링 안됨 return <span>{userName}</span>; } function LoadingIndicator() { const loading = useAppStore(state => state.posts.loading); // user나 ui 변경시 리렌더링 안됨 return loading ? <div>Loading...</div> : null; }

Zustand의 한계

상태 간 연관성이 복잡할 때 모든 관련 상태를 수동으로 동기화해야 합니다.
const useUserStore = create((set) => ({ user: { id: 1, name: 'John' }, posts: [{ authorId: 1, authorName: 'John' }], comments: [{ userId: 1, userName: 'John' }], updateUser: (updates) => { set((state) => { const newUser = { ...state.user, ...updates }; // 연관된 모든 상태를 수동으로 업데이트 const updatedPosts = state.posts.map(post => post.authorId === newUser.id ? { ...post, authorName: newUser.name } : post ); const updatedComments = state.comments.map(comment => comment.userId === newUser.id ? { ...comment, userName: newUser.name } : comment ); return { user: newUser, posts: updatedPosts, comments: updatedComments }; }); } }));

Zustand 사용 시기

다음과 같은 경우에 적합합니다.
  • 중소규모 애플리케이션에서 상태 간 연관성이 복잡하지 않은 경우
  • 명확한 상태 구조로 도메인별 분리가 가능한 경우
  • 성능이 중요한 앱에서 Context API의 불필요한 리렌더링을 피하고 싶은 경우

3️⃣ Valtio와 MobX

Zustand의 수동적 상태 관리 한계를 자동 반응성으로 해결합니다.

자동 상태 동기화

Zustand의 문제: 연관된 상태들을 수동으로 동기화해야 함
// ❌ Zustand - 수동 동기화 updateUser: (updates) => { set((state) => { const newUser = { ...state.user, ...updates }; // 모든 연관 상태를 수동으로 업데이트 const updatedPosts = state.posts.map(post => post.authorId === newUser.id ? { ...post, authorName: newUser.name } : post ); return { user: newUser, posts: updatedPosts }; }); }
Valtio의 해결책: 자연스러운 JavaScript 문법으로 자동 동기화
// ✅ Valtio - 자동 동기화 const state = proxy({ user: { id: 1, name: 'John' }, posts: [{ authorId: 1, authorName: 'John' }], comments: [{ userId: 1, userName: 'John' }] }); function updateUser(updates) { Object.assign(state.user, updates); // 연관된 상태들도 자연스럽게 업데이트 state.posts.forEach(post => { if (post.authorId === state.user.id) { post.authorName = state.user.name; } }); }

자동 Computed 값 관리

Zustand의 문제: computed 값을 매번 재계산하거나 수동 캐싱 필요
// ❌ Zustand - 매번 전체 계산 getUserStats: () => { const { users, posts } = get(); return users.map(user => ({ ...user, postCount: posts.filter(p => p.authorId === user.id).length, totalLikes: posts .filter(p => p.authorId === user.id) .reduce((sum, post) => sum + post.likes, 0) })); }
Valtio/MobX의 해결책: 의존성 자동 추적과 캐싱
// ✅ Valtio - 자동 computed 캐싱 const state = proxy({ users: [], posts: [], get userStats() { return this.users.map(user => ({ ...user, postCount: this.posts.filter(p => p.authorId === user.id).length, totalLikes: this.posts .filter(p => p.authorId === user.id) .reduce((sum, post) => sum + post.likes, 0) })); } // users나 posts가 실제로 변경될 때만 재계산 }); // ✅ MobX - computed 데코레이터 class StatsStore { users = []; posts = []; constructor() { makeObservable(this, { users: observable, posts: observable, userStats: computed }); } get userStats() { return this.users.map(user => ({ ...user, postCount: this.posts.filter(p => p.authorId === user.id).length })); } // 의존성 변경시에만 자동 재계산 }

자동 Fine-grained Reactivity

Zustand의 한계: 선택적 구독을 위해 수동으로 selector 작성 필요
// 🔄 Zustand - 수동 selector 작성 function UserName() { const userName = useStore(state => state.user.name); return <span>{userName}</span>; }
Valtio/MobX의 해결책: 사용되는 속성 자동 추적
// ✅ Valtio - 자동 속성 추적 function UserName() { const snap = useSnapshot(appState); return <span>{snap.user.name}</span>; // user.name 자동 추적 } // ✅ MobX - observer가 자동 추적 const UserName = observer(() => { return <span>{appStore.user.name}</span>; // 사용된 속성 자동 추적 });

4️⃣ 프로젝트별 선택 가이드

단순한 상태 관리

  • Context API: 로그인 상태, 테마, 언어 설정처럼 가끔 바뀌는 전역 상태
  • Zustand: 장바구니, 사용자 설정처럼 자주 바뀌지만 단순한 상태

복잡한 상태 관리

  • Zustand: 폼 데이터, 리스트 필터링처럼 독립적인 상태들
  • Valtio: 사용자 프로필 변경시 댓글 작성자명도 함께 바뀌는 등 연관된 상태들

매우 복잡한 상태 관리

  • Valtio: 실시간 협업 도구, 복잡한 폼 위저드처럼 상태간 의존성이 많은 경우
  • MobX: CRM, ERP처럼 복잡한 비즈니스 규칙과 계산 로직이 많은 경우

마이그레이션 시점

Context API에서 Zustand로 바꾸는 경우
  • Provider Hell이 발생하기 시작할 때
  • 성능 이슈로 인한 불필요한 리렌더링이 문제가 될 때
Zustand에서 Valtio나 MobX로 바꾸는 경우
  • 상태 간 수동 동기화 로직이 복잡해질 때
  • computed 값 관리가 어려워질 때
  • 상태 관련 버그가 자주 발생할 때

Valtio를 선택하면 좋은 경우

  • JavaScript의 자연스러운 문법을 선호하는 팀
  • 깊은 중첩 상태를 많이 다루는 경우
  • React Hooks 패턴에 익숙한 팀
  • 최소한의 학습 비용으로 고급 기능을 원하는 경우

MobX를 선택하면 좋은 경우

  • 객체지향 프로그래밍에 익숙한 팀
  • 복잡한 비즈니스 로직이 많은 애플리케이션
  • 클래스 기반 도메인 모델링을 선호하는 경우
  • computed 값과 액션의 명확한 분리가 필요한 경우

결론

상태관리 라이브러리는 해결하려는 문제의 복잡도에 따라 선택해야 합니다. Context API의 Provider Hell과 성능 문제를 Zustand가 해결했지만, 복잡한 상태 간 관계에서는 한계가 있습니다. Valtio와 MobX의 자동 반응성은 복잡성을 근본적으로 해결하여 개발자가 비즈니스 로직에 집중할 수 있게 해줍니다.
작게 시작해서 필요에 따라 점진적으로 마이그레이션하는 것이 가장 실용적인 접근 방법입니다.