Next.js 상태 관리 선택 가이드: Zustand vs Jotai 실전 비교

오류 메시지가 또 나타났습니다. Redux의 action creator를 수정한 것만 벌써 17번째입니다. 프로젝트에는 장바구니 기능 하나뿐인데 설정 파일은 이미 세 개나 생겼습니다.
왜 이렇게까지 복잡해야 할까요? 프로젝트가 크지도 않은데 Redux를 쓰자니 과도하게 무겁고, Context API로 바꾸자니 상태 하나를 업데이트할 때 페이지의 절반이 다시 렌더링됩니다. 성능 패널을 가득 채운 붉은 표시만 봐도 머리가 아픕니다.
바로 이런 이유로 최근 2년 사이 Zustand와 Jotai가 갑자기 인기를 얻었습니다. 두 라이브러리가 내세우는 가치는 ‘경량’과 ‘고성능’입니다. 그렇다면 둘 중 무엇을 선택해야 할까요? 이 글에서는 다음 내용을 살펴봅니다.
- Redux와 Context가 불편한 이유와 실제 함정
- Zustand와 Jotai의 본질적인 차이
- 상황별 선택 기준과 의사 결정 트리
- Next.js App Router에서 사용하는 방법과 주의할 점
Redux와 Context를 사용하지 않는 이유
Redux의 ‘무거움’은 구체적으로 무엇일까요?
먼저 Redux부터 이야기해 보겠습니다. Redux가 나쁜 도구인 것은 아니지만, 많은 프로젝트에는 지나치게 복잡합니다.
action types, action creators, reducers를 작성하고 store까지 설정해야 합니다. 단순한 ‘장바구니에 추가’ 기능 하나에도 파일 세네 개를 건드릴 수 있습니다. 이런 상용구 코드를 계속 작성하다 보면 정말 번거롭습니다.
더 중요한 문제는 팀에 초보자가 있을 때 Redux의 학습 곡선이 꽤 가파르다는 점입니다. dispatch란 무엇인지, 왜 pure function을 써야 하는지, middleware는 또 무엇을 하는지 등 여러 개념을 이해하는 데 시간이 필요합니다.
할 일 목록이나 개인 블로그 같은 소규모 프로젝트에서 Redux를 쓰는 것은 장을 보러 탱크를 몰고 가는 것과 비슷합니다. 목적지에는 도착하겠지만 그럴 필요는 없습니다.
Context API의 성능 함정
그렇다면 Context는 어떨까요? 확실히 간단하고 React에 기본으로 포함되어 있어 별도 라이브러리를 설치할 필요도 없습니다.
하지만 큰 문제가 하나 있습니다. 바로 성능입니다.
Context의 동작 방식상 Provider의 value가 변경되면 해당 Context를 사용하는 모든 컴포넌트가 다시 렌더링됩니다. 객체의 필드 하나만 사용하더라도 컴포넌트 전체가 렌더링 과정을 다시 거칩니다.
예전에 Context로 필드 상태를 관리하는 폼을 만든 적이 있습니다. 입력창 하나에서 onChange가 발생하자 페이지의 컴포넌트 20개가 전부 다시 렌더링됐습니다. Chrome DevTools의 플레임 차트를 보고 있자니 답답했습니다.
memo, useMemo, Context 분리 같은 기법으로 최적화할 수는 있습니다. 하지만 솔직히 최적화를 마친 코드의 복잡도는 Redux와 크게 다르지 않습니다.
경량 솔루션이 매력적인 이유
바로 이 때문에 Zustand와 Jotai가 인기를 얻었습니다.
두 라이브러리의 약속은 명확합니다.
- API가 단순해 빠르게 배울 수 있습니다. Zustand는 10분이면 시작할 수 있습니다.
- 성능 최적화가 내장되어 있어 직접 처리할 필요가 없습니다.
- 번들 크기가 작습니다. Zustand는 1KB에 불과하며 gzip 후에는 더 작습니다.
데이터도 이를 뒷받침합니다. 2025년 통계에 따르면 Zustand 사용량은 지난 1년 동안 150% 증가했습니다. 점점 더 많은 개발자가 Redux를 떠나 이런 경량 솔루션으로 이동하고 있습니다.
그러나 여기서 새로운 질문이 생깁니다. Zustand와 Jotai 중 무엇을 선택해야 할까요?
Zustand vs Jotai 핵심 차이
겉으로는 둘 다 ‘경량 상태 관리’ 라이브러리처럼 보이지만, 기반 설계 철학은 완전히 다릅니다.
상태 모델: 단일 store vs 개별 atom
공식 문서에는 차이를 명확히 설명하는 문장이 있습니다. “Zustand is like Redux. Jotai is like Recoil.”
Zustand는 본질적으로 단순화한 Redux입니다. 하나의 store 안에서 모든 상태를 관리합니다. 모든 상품을 한곳에서 관리하는 대형 쇼핑몰과 비슷합니다.
// Zustand: 하나의 큰 store
const useStore = create((set) => ({
user: null,
cart: [],
theme: 'light',
// 모든 상태가 여기에 있음
}))
반면 Jotai는 원자형 구조입니다. 각 상태가 독립된 atom으로 존재하며, 각각 따로 운영되는 작은 상점과 같습니다.
// Jotai: 독립된 atoms
const userAtom = atom(null)
const cartAtom = atom([])
const themeAtom = atom('light')
이러한 설계 차이가 각 라이브러리에 적합한 상황을 결정합니다.
저장 위치: 모듈 외부 vs 컴포넌트 트리 내부
Zustand의 store는 모듈 수준에 있으며 React 외부에 존재합니다. Provider 없이도 어디서나 가져와 업데이트할 수 있습니다.
Jotai의 atoms는 컴포넌트 트리 안에 존재하며 Context에 의존합니다. 루트 컴포넌트를 Provider로 감싸야 컴포넌트 사이에서 atoms의 상태를 공유할 수 있습니다.
이 차이는 무엇을 의미할까요?
React 컴포넌트 외부, 예를 들어 유틸리티 함수나 WebSocket 콜백에서 상태를 업데이트해야 한다면 Zustand가 훨씬 편합니다. Jotai도 가능하지만 몇 단계를 더 거쳐야 합니다.
성능 특성: 수동 최적화 vs 자동 최적화
성능에 접근하는 전략도 서로 다릅니다.
Jotai의 원자형 구독은 기본 상태부터 최적화되어 있습니다. 컴포넌트는 사용하는 atoms만 구독하며 다른 atoms의 업데이트는 재렌더링을 일으키지 않습니다.
Zustand에서는 selector를 사용해 최적화해야 합니다.
// 권장하지 않음: 전체 store 구독
const store = useStore()
// 권장: selector로 필요한 부분만 구독
const user = useStore(state => state.user)
그렇다고 Zustand의 selector가 번거로운 것은 아닙니다. 작성법이 간단하고 직관적이지만, 잊지 않고 사용해야 한다는 점에 주의해야 합니다.
한 문장으로 요약
- Zustand: React 외부에 있는 단일 store, selector를 이용한 수동 최적화 필요
- Jotai: React 내부의 원자형 atoms, 자동 성능 최적화
어느 쪽이 더 적합한지는 프로젝트의 특성에 달려 있습니다.
Zustand를 선택해야 하는 상황
먼저 Zustand부터 살펴보겠습니다. 프로젝트가 다음 특성에 해당한다면 Zustand가 대체로 가장 좋은 선택입니다.
복잡성을 피하고 싶은 중소형 애플리케이션
솔직히 대부분의 프로젝트에는 복잡한 상태 관리가 필요하지 않습니다.
전자상거래 웹사이트에서 관리해야 할 전역 상태는 보통 사용자 정보, 장바구니, 테마 설정 정도입니다. 이런 상황에는 Zustand가 잘 맞습니다.
API가 매우 단순해 거의 따로 배울 필요가 없습니다. 전체 예제를 보겠습니다.
// store.js
import create from 'zustand'
const useStore = create((set) => ({
cart: [],
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
removeFromCart: (id) => set((state) => ({
cart: state.cart.filter(item => item.id !== id)
})),
}))
// CartButton.jsx
function CartButton() {
const addToCart = useStore(state => state.addToCart)
return <button onClick={() => addToCart(item)}>장바구니에 추가</button>
}
// CartCount.jsx
function CartCount() {
const count = useStore(state => state.cart.length)
return <span>{count}</span>
}
보셨나요? Provider도, action types도, reducer도 없습니다. 상태와 메서드를 정의한 다음 바로 사용하면 됩니다.
팀의 초보자도 이 코드를 보면 10분 안에 시작할 수 있습니다.
React 외부에서 상태를 업데이트해야 할 때
이는 Zustand만의 독특한 장점입니다.
예를 들어 WebSocket 연결에서 메시지를 받았을 때 상태를 업데이트해야 한다고 가정해 보겠습니다.
// websocket.js
import { useStore } from './store'
socket.on('message', (data) => {
// 컴포넌트 안이 아니어도 store 메서드를 직접 호출
useStore.getState().updateMessages(data)
})
또는 유틸리티 함수에서 현재 상태를 바탕으로 판단해야 할 수도 있습니다.
// utils.js
import { useStore } from './store'
export function checkPermission() {
const user = useStore.getState().user
return user?.role === 'admin'
}
이런 상황에서는 Jotai가 다소 불편합니다. atoms가 컴포넌트 트리에 연결되어 있어 컴포넌트 외부에서 접근하려면 우회해야 합니다.
Next.js SSR과의 좋은 호환성
Zustand는 Next.js 지원이 특히 잘 갖춰져 있습니다.
공식 문서에는 Next.js 통합만 다루는 별도 장이 있으며 App Router 모범 사례도 제공합니다. 커뮤니티에 축적된 문제 해결 경험도 많아 대부분의 문제는 해결 방법을 쉽게 찾을 수 있습니다.
Next.js 13+ App Router를 사용한다면 Zustand는 현재 가장 안정적인 선택지 중 하나입니다. 뒤에서 설정 방법을 자세히 설명하겠습니다.
Zustand가 적합하지 않은 경우
다만 상태 사이에 복잡한 파생 관계가 있다면 Zustand가 최선이 아닐 수 있습니다.
예를 들어 필터가 있고, 필터 조건이 10개이며 각 조건의 선택지가 다른 조건의 현재 값에 따라 달라진다고 가정해 보겠습니다. 이런 경우 Zustand 코드는 꽤 복잡해집니다.
이럴 때 Jotai의 원자형 설계가 빛을 발합니다.
Jotai를 선택해야 하는 상황
Jotai의 원자형 설계는 특정 상황에서 매우 강력합니다.
복잡한 상태 의존 관계
Jotai가 가장 잘하는 영역입니다.
상품 필터를 만든다고 가정해 보겠습니다.
- ‘브랜드’, ‘가격 범위’, ‘평점’ 등의 필터 조건이 있습니다.
- 선택 가능한 브랜드 목록은 현재 가격 범위에 따라 달라집니다.
- 최종 상품 목록은 모든 필터 조건을 함께 반영합니다.
Zustand로 작성하면 이러한 의존 관계를 직접 관리해야 하므로 코드가 쉽게 복잡해집니다.
Jotai로 작성하면 훨씬 명확합니다.
// 기본 atoms
const brandAtom = atom([])
const priceRangeAtom = atom([0, 1000])
const ratingAtom = atom(0)
// 파생 atom: 선택 가능한 브랜드(가격 범위에 의존)
const availableBrandsAtom = atom((get) => {
const priceRange = get(priceRangeAtom)
return fetchBrands(priceRange) // priceRange 변경에 자동으로 반응
})
// 파생 atom: 필터링한 상품(모든 필터 조건에 의존)
const filteredProductsAtom = atom((get) => {
const brands = get(brandAtom)
const priceRange = get(priceRangeAtom)
const rating = get(ratingAtom)
return products.filter(/* 필터링 로직 */)
})
보셨나요? 각 atom은 자신이 의존하는 다른 atoms에만 관심을 두고, Jotai가 의존 관계를 자동으로 추적합니다. 어느 값이 변경되든 관련 atoms가 자동으로 업데이트됩니다.
컴포넌트에서도 간단하게 사용할 수 있습니다.
function FilterPanel() {
const [brands, setBrands] = useAtom(brandAtom)
const availableBrands = useAtomValue(availableBrandsAtom)
// brands가 변경되면 availableBrands가 자동으로 다시 계산됨
}
이런 상황에서는 Jotai 코드가 Zustand보다 훨씬 명확합니다.
최고의 성능이 필요한 상황
Jotai의 원자형 구독은 실제로 성능이 뛰어납니다.
실시간 데이터 대시보드에 서로 다른 지표를 보여 주는 컴포넌트 50개가 있다고 가정해 보겠습니다. Context나 최적화하지 않은 Zustand를 사용하면 데이터 하나를 업데이트할 때 많은 컴포넌트가 다시 렌더링될 수 있습니다.
Jotai에서는 그렇지 않습니다. 각 컴포넌트는 자신의 atom만 구독하므로 다른 atoms의 업데이트는 전혀 영향을 주지 않습니다.
공식 문서에서는 이를 명확히 설명합니다. “This is the most performant by default.” 컴포넌트는 특정 atoms만 구독하며 그 atom이 변경될 때만 다시 렌더링됩니다.
코드 분할이 필요한 대형 애플리케이션
Jotai의 atoms는 필요할 때만 로드할 수 있습니다.
atoms를 여러 파일로 나누고 실제로 사용할 때만 가져올 수 있습니다. 대형 애플리케이션의 초기 화면 로딩에 유용합니다.
Zustand의 store는 보통 하나의 큰 단위입니다. 분할할 수는 있지만 Jotai만큼 자연스럽지는 않습니다.
Suspense를 적극적으로 사용하는 프로젝트
프로젝트에서 React Suspense를 비동기 데이터 로딩 등에 많이 사용한다면 Jotai가 기본 지원을 제공합니다.
비동기 atom은 매우 직관적으로 작성할 수 있습니다.
const userAtom = atom(async () => {
const res = await fetch('/api/user')
return res.json()
})
function UserProfile() {
const user = useAtomValue(userAtom)
// 데이터를 모두 불러올 때까지 자동으로 Suspense 적용
return <div>{user.name}</div>
}
Zustand도 Suspense와 함께 사용할 수 있지만 추가 래퍼가 필요해 Jotai만큼 매끄럽지는 않습니다.
Jotai의 학습 비용
다만 Jotai의 개념은 Zustand보다 조금 더 복잡합니다.
atom 읽기와 쓰기, 파생 atom, 비동기 atom 같은 개념을 이해하려면 시간이 조금 필요합니다. 팀에 초보자가 있다면 Zustand보다 시작하는 데 더 오래 걸릴 수 있습니다.
솔직히 Jotai 문서도 Zustand만큼 친절하지는 않습니다. 여러 API의 사용법을 파악하려면 몇 번 더 읽어야 할 수 있습니다.
Next.js App Router 모범 사례
이론을 살펴봤으니 이제 실전으로 들어가겠습니다. Next.js 13+ App Router에서 두 라이브러리를 사용할 때는 미리 알아야 할 몇 가지 함정이 있습니다.
Next.js에서 Zustand를 올바르게 사용하는 방법
중요한 함정: 전역 store를 사용하지 마세요.
많은 사람이 처음에는 저를 포함해 다음과 같이 작성합니다.
// ❌ 잘못된 예: 전역 store
import create from 'zustand'
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}))
클라이언트 렌더링에서는 문제가 없지만 Next.js의 SSR 환경에서는 이 store가 여러 요청 사이에 공유됩니다. 사용자 A의 데이터가 사용자 B에게 노출될 수 있어 보안 문제가 됩니다.
공식적으로 권장하는 방식은 Store Factory 패턴입니다.
// lib/store.js
import { createStore } from 'zustand/vanilla'
export function createUserStore(initialState) {
return createStore((set) => ({
user: initialState?.user || null,
setUser: (user) => set({ user })
}))
}
그런 다음 클라이언트 컴포넌트에서 Provider를 만듭니다.
// components/StoreProvider.jsx
'use client'
import { createContext, useContext, useRef } from 'react'
import { useStore } from 'zustand'
import { createUserStore } from '@/lib/store'
const StoreContext = createContext(null)
export function StoreProvider({ children, initialState }) {
const storeRef = useRef()
if (!storeRef.current) {
storeRef.current = createUserStore(initialState)
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
}
export function useUserStore(selector) {
const store = useContext(StoreContext)
return useStore(store, selector)
}
루트 레이아웃에서 사용합니다.
// app/layout.jsx
import { StoreProvider } from '@/components/StoreProvider'
export default function RootLayout({ children }) {
// 여기서 데이터를 미리 가져올 수 있음
const initialState = { user: null }
return (
<html>
<body>
<StoreProvider initialState={initialState}>
{children}
</StoreProvider>
</body>
</html>
)
}
이렇게 하면 요청마다 독립된 store를 사용하므로 데이터가 섞이지 않습니다.
주의 사항:
- Server Components에서는 store를 직접 읽거나 쓸 수 없습니다.
- 서버에서 데이터를 미리 가져온 뒤 initialState로 클라이언트에 전달합니다.
- 루트 레이아웃에서 블로킹 방식으로 데이터를 가져오면 성능에 영향을 줄 수 있으므로 피하세요.
Jotai의 SSR Hydration 문제
Next.js에서 Jotai를 사용할 때 가장 큰 함정은 hydration 오류입니다.
반드시 요청마다 독립된 Provider를 만들어야 합니다.
// app/providers.jsx
'use client'
import { Provider } from 'jotai'
export function Providers({ children }) {
return <Provider>{children}</Provider>
}
// app/layout.jsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
서버 데이터 처리:
서버 데이터를 atoms에 주입해야 한다면 useHydrateAtoms를 사용합니다.
'use client'
import { useHydrateAtoms } from 'jotai/utils'
import { userAtom } from '@/atoms'
export function HydrateAtoms({ initialUser, children }) {
useHydrateAtoms([[userAtom, initialUser]])
return children
}
하지만 주의할 점이 있습니다. useHydrateAtoms는 첫 렌더링에서만 적용됩니다. App Router에서 router.push로 이동한 뒤 같은 atom을 두 번째로 방문하면 다시 hydrate되지 않습니다.
해결 방법:
- Provider를
layout.tsx대신template.tsx에 배치해 라우팅할 때마다 다시 생성합니다. - 전역 Provider 대신 페이지 수준 Provider를 사용합니다.
atomWithStorage의 hydration 오류:
atomWithStorage로 폼 데이터를 저장할 때 서버 렌더링 결과와 클라이언트 hydration 결과가 일치하지 않을 수 있습니다.
- 서버: 폼이 비어 있음
- 클라이언트: localStorage에서 값을 읽어 폼에 데이터가 있음
- 결과: React hydration mismatch 오류
해결하려면 useEffect에서 폼을 채우거나 useHydrateAtoms와 useSyncExternalStore를 함께 사용하세요.
두 라이브러리에 공통으로 적용되는 원칙
-
Server Components에서는 상태 관리를 사용할 수 없습니다.
- Server Components에는 hooks가 없어
useStore나useAtom을 사용할 수 없습니다. - 상태가 필요한 컴포넌트에는
'use client'를 지정합니다.
- Server Components에는 hooks가 없어
-
Provider 위치가 중요합니다.
- 가능한 한 깊이 배치하면 Next.js가 정적 부분을 최적화하는 데 도움이 됩니다.
- 다만 상태가 필요한 모든 컴포넌트가 편리하게 접근할 수 있어야 합니다.
-
루트 레이아웃을 블로킹하지 마세요.
- root layout에서 사용자 데이터를 가져오기 위해
await fetch()를 사용하지 마세요. - streaming과 Server Components의 성능 이점을 상쇄합니다.
- 전용 클라이언트 컴포넌트에서 데이터 가져오기와 초기화를 처리하세요.
- root layout에서 사용자 데이터를 가져오기 위해
저도 이 함정들을 모두 겪어 봤습니다. 이 원칙을 기억하면 디버깅 시간을 크게 줄일 수 있습니다.
나의 선택 제안
여기까지 읽고도 어느 쪽을 선택해야 할지 궁금할 수 있습니다.
솔직히 정답은 없습니다. 다만 의사 결정 트리를 제시할 수는 있습니다.
빠른 의사 결정 트리
프로젝트가 다음과 같다면…
-
단순한 개인 프로젝트 또는 소형 애플리케이션
→ 먼저 Context API를 사용하고 충분하면 바꾸지 마세요.
→ 성능 문제가 생기면 Zustand를 도입하세요. -
중형 SaaS 애플리케이션 또는 전자상거래 웹사이트
→ 바로 Zustand를 사용하세요.
→ 단순하고 안정적이며 팀이 쉽게 배울 수 있습니다. -
복잡한 데이터 대시보드 또는 실시간 애플리케이션
→ Jotai를 고려하세요.
→ 상태 의존 관계가 복잡할수록 Jotai가 더 명확합니다. -
대규모 팀 또는 엄격한 규칙이 필요한 프로젝트
→ Redux Toolkit이 더 적합할 수 있습니다.
→ 더 많은 제약과 모범 사례를 제공합니다. -
React 외부에서 상태를 업데이트해야 하는 경우
→ Zustand가 유일한 선택에 가깝습니다.
→ Jotai도 가능하지만 자연스럽지는 않습니다. -
Suspense를 적극적으로 사용하는 경우
→ Jotai가 더 매끄럽습니다.
→ 비동기 atoms를 기본 지원합니다.
점진적 전략
저는 점진적인 선택 전략을 권합니다.
-
1단계: Context API
- 프로젝트 초기이고 상태가 많지 않다면 Context부터 사용합니다.
- 충분하다면 바꾸지 말고 너무 일찍 최적화하지 마세요.
-
2단계: Zustand
- Context의 성능 문제가 나타나기 시작했거나
- 전역 상태가 복잡해졌다면
- Zustand가 80%의 상황을 해결해 줍니다.
-
3단계: Jotai 또는 Zustand 유지
- 상태 의존 관계가 매우 복잡하다면 Jotai를 사용합니다.
- 그렇지 않으면 Zustand를 유지하면 됩니다.
- 기술 자체를 위해 기술을 선택하지 마세요.
함께 사용해도 될까요?
물론입니다!
Zustand와 Jotai는 충돌하지 않습니다. 다음처럼 사용하는 프로젝트도 봤습니다.
- Zustand로 사용자와 테마 같은 전역 설정 관리
- Jotai로 복잡한 폼 상태 관리
전혀 문제없습니다. 구체적인 문제에 가장 적합한 도구를 선택하면 됩니다.
나의 실전 경험
제가 직접 선택한 사례를 공유하겠습니다.
- 개인 블로그: 상태 관리 라이브러리를 사용하지 않습니다. Server Components와 URL state로 충분합니다.
- 관리자용 프로젝트: Zustand를 사용하고 서버 상태는 React Query로 처리합니다.
- 실시간 데이터 대시보드: 상태 의존 관계가 너무 복잡해 Zustand로는 코드가 꼬이므로 Jotai를 사용합니다.
핵심은 프로젝트를 시작할 때부터 기술 선택을 지나치게 고민하지 않는 것입니다. 가장 단순한 방안으로 시작하고 문제가 생길 때 업그레이드하세요.
많은 프로젝트에는 Context API만으로도 충분합니다. Context를 과소평가하지 마세요.
결론
처음의 질문으로 돌아가 보겠습니다.
Redux가 너무 무거운가요? 많은 프로젝트에서는 확실히 과도한 도구입니다.
Context의 성능이 나쁜가요? 최적화하지 않으면 재렌더링이 실제 병목이 될 수 있습니다.
Zustand와 Jotai 중 무엇을 선택해야 할까요? 상황에 따라 다릅니다.
- 대부분의 경우에는 단순하고 안정적인 Zustand로 충분합니다.
- 복잡한 상태 의존 관계에는 Jotai가 더 우아합니다.
- 확신이 없다면 Zustand부터 시작하고 정말 부족할 때 바꾸세요.
Next.js App Router에서는 어떻게 사용해야 할까요? 세 가지만 기억하세요.
- 전역 store를 사용하지 마세요.
- 요청마다 독립된 Provider를 사용하세요.
- Server Components에서는 상태를 다루지 마세요.
마지막으로 한마디만 덧붙이겠습니다.
기술 선택에는 정답이 없습니다. 인터넷의 비교 글에 끌려다니지 마세요. 이 글도 마찬가지입니다. 가장 중요한 것은 자신과 팀이 편안하게 사용할 수 있고 실제 문제를 해결하는 도구를 고르는 것입니다.
지금 고민 중이라면 하나를 골라 demo를 작성해 보세요. 10분 동안 직접 해보는 것이 비교 글 10편을 읽는 것보다 낫습니다.
프로젝트에 잘 맞는 도구를 선택하시길 바랍니다.
FAQ
Redux, Context, Zustand, Jotai는 어떻게 다른가요?
• 장점: 기능이 강력하고 생태계가 풍부해 초대형 프로젝트에 적합합니다.
• 단점: 무겁고 상용구 코드가 많으며 학습 비용이 높습니다.
• 적합한 경우: 복잡한 상태 관리가 필요한 초대형 프로젝트
Context API:
• 장점: React에 내장되어 있어 별도 라이브러리가 필요 없습니다.
• 단점: 성능이 좋지 않아 하나의 상태 업데이트가 전체 하위 트리를 다시 렌더링할 수 있습니다.
• 적합한 경우: 자주 변경되지 않는 단순한 전역 상태
Zustand:
• 장점: 간단하고 직관적이며 코드가 적고 학습 비용이 낮습니다.
• 단점: 초대형 프로젝트에는 적합하지 않습니다.
• 적합한 경우: 대부분의 프로젝트와 중소형 프로젝트
Jotai:
• 장점: 원자형 상태와 세밀한 업데이트를 지원해 성능이 좋습니다.
• 단점: 학습 비용이 높고 코드가 더 복잡합니다.
• 적합한 경우: 최고의 성능이 필요한 대형 프로젝트
선택 제안: 대부분의 프로젝트에는 Zustand를, 대형 프로젝트나 최고의 성능이 필요할 때는 Jotai를 선택하세요.
언제 Zustand를 사용하고 언제 Jotai를 사용해야 하나요?
• 대부분의 프로젝트
• 중소형 프로젝트
• 간단하고 직관적인 상태 관리가 필요한 경우
• 팀의 학습 비용을 낮춰야 하는 경우
• 적은 코드가 중요한 경우
Jotai를 선택할 상황:
• 대형 프로젝트
• 최고의 성능이 필요한 경우
• 상태가 자주 업데이트되는 경우
• 세밀한 업데이트가 필요한 경우
• 팀에 관련 경험이 있는 경우
의사 결정 트리:
• 프로젝트 규모가 작음 → Zustand
• 프로젝트 규모가 큼 → Jotai
• 성능 요구 사항이 높음 → Jotai
• 단순성과 사용 편의성이 중요함 → Zustand
제안: 대부분의 프로젝트에는 Zustand를 선택하고, 대형 프로젝트이거나 최고의 성능이 필요할 때만 Jotai를 선택하세요.
Context API의 성능이 좋지 않은 이유는 무엇인가요?
해결 방법:
• Context 분리(기능별로 서로 다른 Context 사용)
• useMemo와 memo를 사용해 최적화
• 세밀한 구독을 지원하는 Zustand 또는 Jotai로 전환
제안: 상태가 자주 업데이트된다면 Context API를 권장하지 않으며 Zustand 또는 Jotai를 사용하는 편이 좋습니다.
Next.js App Router에서 Zustand를 어떻게 사용하나요?
1. createStore로 store 팩토리 함수 생성
2. 클라이언트 컴포넌트에서 useRef로 store 인스턴스 생성
3. Context Provider를 통해 인스턴스를 하위 컴포넌트에 전달
핵심 사항:
• store를 사용하는 컴포넌트에는 반드시 'use client'를 지정해야 합니다.
• Server Components에서는 상태 관리를 사용할 수 없습니다.
• 데이터가 섞이지 않도록 요청마다 독립된 store 인스턴스를 사용해야 합니다.
이렇게 하면 요청마다 독립적인 상태를 보장하면서도 Zustand의 간결함을 유지할 수 있습니다.
Next.js App Router에서 Jotai를 어떻게 사용하나요?
1. 루트 레이아웃이나 template.jsx에서 Provider 컴포넌트로 감싸기
2. useHydrateAtoms로 서버 데이터를 초기값으로 주입
3. 주의: useHydrateAtoms는 첫 렌더링에서만 적용됩니다.
핵심 사항:
• atom을 사용하는 컴포넌트는 반드시 Client Component여야 합니다.
• Server Components에서는 상태 관리를 사용할 수 없습니다.
• 원자형 설계로 세밀한 업데이트가 자동으로 구현됩니다.
장점:
• 특정 atom을 구독하는 컴포넌트만 업데이트됩니다.
• Suspense와 비동기 데이터를 기본 지원합니다.
• 복잡한 상태 의존 관계와 대형 프로젝트에 적합합니다.
주의: Jotai는 Zustand보다 학습 비용이 높지만 복잡한 상황에서는 성능과 코드 명확성이 더 좋습니다.
Server Components에서 상태 관리를 사용할 수 있나요?
올바른 방법:
• Server Components는 데이터 가져오기(fetch, 데이터베이스 쿼리 등)를 담당합니다.
• props로 데이터를 Client Components에 전달합니다.
• Client Components는 상태 관리와 사용자 상호작용을 담당합니다.
아키텍처 패턴:
Server Component가 초기 데이터 가져오기 → props로 전달 → Client Component가 Zustand/Jotai로 상태와 상호작용 관리
이 역할 분담을 통해 서버 컴포넌트는 데이터 가져오기와 SEO에 집중하고, 클라이언트 컴포넌트는 상호작용과 상태 관리에 집중하여 Next.js App Router의 장점을 충분히 활용할 수 있습니다.
4분 읽기 · 게시일: 2025년 12월 19일 · 수정일: 2026년 9월 4일
Next.js 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Next.js + Prisma 완벽 입문 가이드: 설정부터 실전까지(연결 누수 해결법 포함)
환경 설정, Schema 설계, CRUD 실습, 핫 리로드 연결 누수 해결법까지 다루는 Next.js + Prisma 종합 입문 튜토리얼입니다. Prisma ORM을 빠르게 익히려는 개발자를 위한 안내서입니다.
45편 중 20편
다음
Next.js 캐시와 revalidate 완전 정리: 데이터가 갱신되지 않을 때 해결하는 법
Next.js의 네 가지 캐시 계층과 revalidate, revalidatePath, revalidateTag의 사용 시점을 설명하고 데이터가 갱신되지 않는 문제를 단계별로 해결합니다.
45편 중 22편



댓글
GitHub로 로그인하여 댓글을 남기세요