Zustand vs Jotai: 2024년, 당신의 리액트 상태 관리 라이브러리는?
리액트 프로젝트에서 상태 관리는 언제나 중요한 고민거리입니다. Redux의 복잡함에 지쳤다면, 미니멀리스트 상태 관리 라이브러리인 Zustand와 Jotai가 훌륭한 대안이 될 수 있습니다. 이 글에서는 두 라이브러리의 특징, 장단점, 실용적인 코드 예시를 통해 어떤 상황에 더 적합한지 심층적으로 비교 분석합니다.
Zustand vs Jotai: 2024년, 당신의 리액트 상태 관리 라이브러리는?
안녕하세요, 프런트엔드 개발자이자 테크 블로거 닉입니다. 리액트 애플리케이션을 개발하면서 상태 관리는 언제나 중요한 주제입니다. Redux, MobX와 같은 전통적인 라이브러리들이 강력한 기능을 제공하지만, 때로는 과도한 보일러플레이트와 러닝 커브로 인해 개발 피로도를 높이기도 합니다.
최근 몇 년간, 더 가볍고 직관적인 미니멀리스트 상태 관리 라이브러리들이 인기를 얻고 있습니다. 그중에서도 Zustand와 Jotai는 간결한 API, 뛰어난 성능, 그리고 개발자 친화적인 경험으로 많은 주목을 받고 있습니다. 이 두 라이브러리는 각각 다른 접근 방식을 가지고 있어, 어떤 프로젝트에 어떤 라이브러리가 더 적합할지 고민하는 분들이 많을 텐데요. 오늘 이 글에서 Zustand와 Jotai의 특징, 장단점, 그리고 실용적인 코드 예시를 통해 여러분의 선택에 도움을 드리고자 합니다.
Zustand: 간결함 속의 강력함 (v4.5.0)
Zustand는 Redux와 유사한 '단일 스토어' 개념을 사용하지만, 훨씬 적은 보일러플레이트와 간결한 API를 제공합니다. 훅(Hook) 기반으로 설계되어 리액트의 철학과 자연스럽게 어우러지며, 작은 번들 사이즈와 뛰어난 성능을 자랑합니다.
Zustand의 장점
- 간단한 API:
create함수 하나로 스토어를 정의하고, 훅처럼useStore를 사용하여 상태에 접근합니다. - 보일러플레이트 최소화: Redux와 달리 액션, 리듀서, 미들웨어 설정 등의 복잡한 과정이 필요 없습니다.
- 높은 성능:
useStore의 셀렉터(selector) 기능을 통해 필요한 상태만 구독하여 불필요한 리렌더링을 최소화합니다. - 미들웨어 지원: Redux DevTools, persist, immer 등 유용한 미들웨어를 쉽게 추가할 수 있습니다.
- 타입스크립트 친화적: 타입 추론이 잘 되어 있어 타입 안정성을 확보하기 용이합니다.
Zustand의 단점
- 구조화의 유연성: 자유도가 높아 대규모 프로젝트에서는 상태 로직을 어떻게 구성할지 개발팀 내에서 명확한 컨벤션이 필요할 수 있습니다.
- 셀렉터 메모이제이션: 내장된
shallow비교 외에 더 복잡한 객체 비교를 위해서는memo또는createSelector와 같은 추가적인 최적화가 필요할 수 있습니다.
Zustand 코드 예시: 간단한 카운터
1// src/stores/counterStore.js 2import { create } from 'zustand'; 3 4// Zustand v4.5.0 기준 5const useCounterStore = create((set) => ({ 6 count: 0, 7 increment: () => set((state) => ({ count: state.count + 1 })), 8 decrement: () => set((state) => ({ count: state.count - 1 })), 9 reset: () => set({ count: 0 }), 10})); 11 12export default useCounterStore;
1// src/components/ZustandCounter.jsx 2import React from 'react'; 3import useCounterStore from '../stores/counterStore'; 4 5function ZustandCounter() { 6 // 필요한 상태와 액션만 선택하여 구독합니다. 7 // 'shallow'를 사용하여 객체 비교를 최적화할 수 있습니다. 8 const { count, increment, decrement, reset } = useCounterStore( 9 (state) => ({ 10 count: state.count, 11 increment: state.increment, 12 decrement: state.decrement, 13 reset: state.reset, 14 }), 15 // (oldState, newState) => oldState.count === newState.count // 필요에 따라 커스텀 비교 함수 사용 16 ); 17 18 return ( 19 <div> 20 <h3>Zustand 카운터</h3> 21 <p>Count: {count}</p> 22 <button onClick={increment}>증가</button> 23 <button onClick={decrement}>감소</button> 24 <button onClick={reset}>초기화</button> 25 </div> 26 ); 27} 28 29export default ZustandCounter;
Jotai: 원자(Atom) 기반의 세밀한 제어 (v1.13.1)
Jotai는 Recoil에서 영감을 받은 '원자(Atom)' 기반의 상태 관리 라이브러리입니다. 상태를 작은 단위의 원자로 쪼개어 관리하며, 각 원자는 독립적으로 읽고 쓸 수 있습니다. 이를 통해 매우 세밀한 리렌더링 최적화와 유연한 상태 파생(derived state)이 가능합니다.
Jotai의 장점
- 세밀한 리렌더링: 특정 원자가 업데이트되면 해당 원자를 구독하는 컴포넌트만 리렌더링되어 성능 최적화에 매우 유리합니다.
- 파생 상태(Derived State): 다른 원자를 기반으로 새로운 상태를 쉽게 파생시킬 수 있어 복잡한 계산이나 비동기 로직 처리에 강력합니다.
- Suspense 지원: 비동기 원자를 사용하여 데이터 페칭 로직을 React Suspense와 자연스럽게 통합할 수 있습니다.
- 타입스크립트 우선: 처음부터 타입스크립트를 염두에 두고 설계되어 강력한 타입 추론과 안정성을 제공합니다.
- 미니멀리즘: API가 매우 간결하며, 리액트의
useState와 유사한 경험을 제공합니다.
Jotai의 단점
- 원자 개념: 원자 기반의 사고방식에 익숙해지는 데 시간이 필요할 수 있습니다.
- 작은 파일 분리: 원자 하나당 파일 하나로 관리하는 방식이 일반적이므로, 파일 수가 많아질 수 있습니다.
- Provider 필요성: 기본적으로 Provider 없이도 동작하지만, 특정 기능(예: 여러 개의 독립적인 스토어)을 사용하려면
<Provider>를 설정해야 합니다.
Jotai 코드 예시: 파생 상태를 이용한 카운터
1// src/atoms/counterAtom.js 2import { atom } from 'jotai'; 3 4// Jotai v1.13.1 기준 5export const countAtom = atom(0); 6 7// 파생 상태: countAtom의 값이 홀수인지 짝수인지 알려주는 atom 8export const isEvenAtom = atom( 9 (get) => get(countAtom) % 2 === 0 10); 11 12// 액션 역할을 하는 atom (선택 사항, 컴포넌트에서 직접 set 가능) 13export const incrementAtom = atom( 14 null, 15 (get, set) => set(countAtom, get(countAtom) + 1) 16); 17 18export const decrementAtom = atom( 19 null, 20 (get, set) => set(countAtom, get(countAtom) - 1) 21); 22 23export const resetAtom = atom( 24 null, 25 (get, set) => set(countAtom, 0) 26);
1// src/components/JotaiCounter.jsx 2import React from 'react'; 3import { useAtom } from 'jotai'; 4import { countAtom, isEvenAtom, incrementAtom, decrementAtom, resetAtom } from '../atoms/counterAtom'; 5 6function JotaiCounter() { 7 const [count] = useAtom(countAtom); 8 const [isEven] = useAtom(isEvenAtom); // 파생 상태 구독 9 const [, increment] = useAtom(incrementAtom); 10 const [, decrement] = useAtom(decrementAtom); 11 const [, reset] = useAtom(resetAtom); 12 13 return ( 14 <div> 15 <h3>Jotai 카운터</h3> 16 <p>Count: {count}</p> 17 <p>홀수/짝수: {isEven ? '짝수' : '홀수'}</p> 18 <button onClick={increment}>증가</button> 19 <button onClick={decrement}>감소</button> 20 <button onClick={reset}>초기화</button> 21 </div> 22 ); 23} 24 25export default JotaiCounter;
Zustand vs Jotai: 심층 비교
두 라이브러리는 모두 미니멀하고 성능이 뛰어나지만, 상태를 관리하는 근본적인 철학에서 차이를 보입니다.
| 특징 | Zustand (v4.5.0) | Jotai (v1.13.1) |
|---|---|---|
| 패러다임 | 단일 스토어 (Store-based) | 원자 (Atom-based) |
| 러닝 커브 | 매우 낮음 (훅과 유사, 즉각적인 학습) | 낮음 (원자 개념 익숙해지면 강력하고 직관적) |
| 번들 사이즈 | 약 1KB (매우 작음) | 약 2KB (매우 작음) |
| 성능 최적화 | 셀렉터 사용으로 불필요한 리렌더링 최소화 | 원자 업데이트 시 해당 원자 구독 컴포넌트만 리렌더링 |
| 주요 특징 | 미들웨어 지원, 리덕스 데브툴즈 연동 용이, 전역 상태 | 파생 상태, 비동기 상태 관리, Suspense 지원, 로컬 상태 |
| 활용 분야 | 전역 상태, 비교적 큰 단위의 상태 변화, Redux 대체 | 세밀한 컴포넌트 상태, 파생 상태, 비동기 데이터 페칭 |
흔히 저지르는 실수 (Common Mistakes)
Zustand
-
셀렉터 없이
useStore()사용:useStore()를 인자 없이 사용하면 스토어 내의 어떤 상태라도 변경될 때마다 해당 컴포넌트가 리렌더링됩니다. 반드시 셀렉터 함수를 사용하여 필요한 상태만 선택적으로 구독해야 합니다.1// Bad: 스토어의 모든 상태 변화에 리렌더링 2const state = useCounterStore(); 3 4// Good: count만 변경될 때 리렌더링 5const count = useCounterStore(state => state.count); 6 7// Good: 여러 상태를 선택하고 얕은 비교 (shallow compare) 사용 8const { count, increment } = useCounterStore( 9 (state) => ({ count: state.count, increment: state.increment }), 10 shallow 11); -
상태 직접 변경 (Mutation): Zustand는 불변성(immutability)을 권장합니다.
set함수 내에서 상태 객체를 직접 변경하지 않고, 항상 새로운 객체를 반환해야 합니다.1// Bad: 배열/객체를 직접 변경 2set(state => { state.items.push(newItem); return state; }); 3 4// Good: 새로운 배열/객체 반환 5set(state => ({ items: [...state.items, newItem] }));
Jotai
-
컴포넌트 내에서
atom정의:atom은 컴포넌트 외부에서 정의되어야 합니다. 컴포넌트 내에서atom을 정의하면 컴포넌트가 리렌더링될 때마다 새로운atom인스턴스가 생성되어 상태가 초기화되거나 예상치 못한 동작을 유발할 수 있습니다.1// Bad: 컴포넌트 내에서 atom 정의 2function MyComponent() { 3 const myAtom = atom(0); // 매 렌더링마다 새로운 atom 생성 4 const [value] = useAtom(myAtom); 5 // ... 6} 7 8// Good: 컴포넌트 외부에서 atom 정의 9const myAtom = atom(0); 10function MyComponent() { 11 const [value] = useAtom(myAtom); 12 // ... 13} -
불필요한
<Provider>사용: Jotai는 기본적으로<Provider>없이도 동작합니다.<Provider>는 여러 개의 독립적인 스토어를 관리하거나, 서버 사이드 렌더링(SSR) 환경에서 초기 상태를 주입하는 등 특정 고급 시나리오에서만 필요합니다. 대부분의 경우 전역 상태 관리에는 필요하지 않습니다.
언제 무엇을 선택할까?
-
Zustand를 선택할 경우:
- Redux와 유사한 '전역 스토어' 개념에 익숙하며, 보일러플레이트 없이 빠르고 직관적으로 전역 상태를 관리하고 싶을 때.
- 상태의 변경이 비교적 큰 단위로 이루어지며, 명확한 액션 함수를 통해 상태를 조작하고 싶을 때.
- 미들웨어를 활용하여 로깅, 비동기 처리, 영속화(persist) 등의 기능을 쉽게 추가하고 싶을 때.
-
Jotai를 선택할 경우:
- 컴포넌트 레벨의 상태 관리부터 전역 상태까지, 모든 상태를 '원자' 단위로 세밀하게 제어하고 싶을 때.
- 다른 원자로부터 파생되는 복잡한 계산 상태(derived state)나 비동기 상태를 효율적으로 관리해야 할 때.
- React Suspense와 함께 비동기 데이터 페칭 로직을 깔끔하게 통합하고 싶을 때.
- 불필요한 리렌더링을 극도로 줄여야 하는 고성능 애플리케이션을 개발할 때.
💡 핵심 정리: Zustand는 전역 상태 관리에 직관적이고 강력한 선택이며, Jotai는 컴포넌트 레벨의 세밀한 상태 관리와 파생 상태에 탁월합니다. 두 라이브러리 모두 미니멀하고 높은 성능을 제공하지만, 프로젝트의 특성과 팀의 선호도에 따라 현명한 선택을 내리세요!
- ✅ Zustand는 전역 상태 관리에 최적화된 심플한 스토어 기반 라이브러리입니다.
- ✅ Jotai는 원자 단위로 세밀하게 상태를 관리하며, 파생 상태와 높은 성능이 강점입니다.
- ✅ 두 라이브러리 모두 미니멀하고 높은 성능을 제공하지만, 접근 방식에 차이가 있습니다.
- ✅ 프로젝트의 규모, 상태의 복잡성, 팀의 선호도를 고려하여 선택하는 것이 중요합니다.
다음 단계
이 글이 여러분의 상태 관리 라이브러리 선택에 도움이 되었기를 바랍니다. 가장 좋은 방법은 직접 경험해보는 것입니다. 😉
- 🚀 작은 토이 프로젝트에서 두 라이브러리를 직접 경험해보세요!
- 📚 각 라이브러리의 공식 문서를 심도 있게 탐색해보세요. (Zustand, Jotai)
- 💬 팀원들과 함께 프로젝트에 가장 적합한 라이브러리에 대해 논의해보세요.
- 💡 여러분만의 베스트 프랙티스를 찾아내고 공유해주세요!
궁금한 점이나 의견이 있다면 댓글로 남겨주세요! 함께 성장하는 개발 문화, 언제나 환영합니다. Happy Coding! ✨