| 라이브러리 | 주요 특징 | 상태 저장 방식 | 성능 | 사용 난이도 | 새로고침 후 유지 | 비동기 지원 |
| useContext + useReducer | 기본 내장, 간단함 | 메모리 (React 상태) | 중간 | 쉬움 | ❌ 초기화됨 | ❌ 직접 구현 필요 |
| Zustand | 가볍고 빠른 전역 상태 관리 | 메모리 (React 상태) | ✅ 매우 빠름 | ✅ 쉬움 | ❌ 초기화됨 | ✅ 가능 (비동기 지원) |
| Jotai | Recoil-like Atom 상태 관리 | 메모리 (React 상태) | ✅ 빠름 | ✅ 쉬움 | ❌ 초기화됨 | ✅ 가능 (비동기 지원) |
| Recoil | 공식적으로 Facebook이 개발 | 메모리 (React 상태) | ✅ 빠름 | ⚠️ 중간 | ❌ 초기화됨 | ✅ 가능 |
| Redux Toolkit | 전통적인 상태 관리 | 메모리 (Redux Store) | 🚀 최적화 가능 | ⚠️ 중간 | ❌ 초기화됨 | ✅ 가능 (비동기 지원) |
| Nuqs | URL을 상태처럼 사용 | URL Query String | ✅ 빠름 | ✅ 쉬움 | ✅ 유지됨 | ❌ 직접 구현 필요 |
- 간단한 전역 상태 → useContext + useReducer
- 가볍고 빠른 상태 관리 → Zustand
- Atom 기반 세밀한 상태 관리 → Jotai
- 전통적인 Redux 스타일, 복잡한 상태 관리 (대규모 앱) → Redux Toolkit
- URL 기반 상태 관리, URL 기반 상태 (검색 필터, 페이지네이션) → Nuqs
각 라이브러리별 특징 및 예제
1) useContext + useReducer (기본 React 전역 상태 관리)
작은 프로젝트에는 적합
상태가 많아지면 비효율적
✔ 장점
React 기본 기능 (외부 라이브러리 필요 없음)
구조가 단순하여 학습이 쉬움
❌ 단점
성능 이슈 (컴포넌트가 많이 리렌더링됨)
상태가 커질수록 관리가 어려움
예제
import { createContext, useReducer, useContext } from 'react';
// 상태 정의
const CounterContext = createContext(null);
const reducer = (state, action) => (action.type === 'INCREMENT' ? state + 1 : state);
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(reducer, 0);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// 상태 사용
export function Counter() {
const { state, dispatch } = useContext(CounterContext);
return (
<div>
<p>Count: {state}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
</div>
);
}
2) Zustand (가볍고 빠른 전역 상태 관리)
가볍고 빠른 전역 상태 관리
Context보다 성능 우수
URL 상태 저장 불가 → URL 기반 상태는 Nuqs 사용
✔ 장점
Redux보다 가벼움 (~1KB)
React Context보다 성능이 좋음
비동기 지원 가능
❌ 단점
SSR에서 초기 상태를 가져오려면 설정 필요
예제
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export function Counter() {
const { count, increment } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}
Zustand
초경량 (~1KB) & 빠른 상태 업데이트
불필요한 리렌더링 방지 (React Context보다 성능 우수)
비동기 지원 가능 (fetch API, Zustand + React Query 사용 가능)
React Native 및 Next.js에서도 사용 가능
1. 왜 Zustand를 사용할까?
React의 상태 관리 문제점
- useState → 컴포넌트 간 상태 공유 어려움
- useContext → 컴포넌트가 많아지면 성능 저하 (리렌더링 문제 발생)
- Redux → 보일러플레이트 코드 많고 복잡함
2. Zustand 기본 사용법
설치
npm install zustand
Zustand는 create 함수를 사용하여 **스토어(Store)**를 만든다.
1️⃣ 간단한 전역 상태 관리 (Counter 예제)
import create from 'zustand';
// Zustand 스토어 생성
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export default function Counter() {
const { count, increment } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}
전역적으로 count 상태 관리 가능
Redux보다 훨씬 간결한 코드
useContext 없이도 상태 공유 가능
3. Zustand의 주요 기능
1️⃣ 여러 개의 상태 관리
const useStore = create((set) => ({
count: 0,
user: 'John',
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (newUser) => set(() => ({ user: newUser })),
}));
export default function App() {
const { count, increment, user, setUser } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<p>User: {user}</p>
<button onClick={() => setUser('Alice')}>Change User</button>
</div>
);
}
여러 개의 상태(count, user)를 한 번에 관리 가능
Context 없이 전역 상태 관리 가능
2️⃣ 비동기 데이터 처리 (API 요청)
Zustand는 비동기 API 호출(fetch, axios)도 쉽게 지원한다.
const useStore = create((set) => ({
user: null,
fetchUser: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await response.json();
set({ user: data });
},
}));
export default function UserProfile() {
const { user, fetchUser } = useStore();
return (
<div>
<button onClick={fetchUser}>Load User</button>
{user && <p>Name: {user.name}</p>}
</div>
);
}
Zustand는 useEffect 없이도 비동기 API 호출 가능
Redux에서 redux-thunk가 필요했던 것을 한 줄 코드로 해결
3️⃣ 특정 상태만 구독 (useStore.subscribe)
- useStore()로 모든 상태를 불러오면 불필요한 리렌더링 발생 가능
- subscribe을 사용하면 특정 상태만 감지할 수 있음
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
useStore.subscribe((state) => {
console.log('Count 변경됨:', state.count);
});
특정 상태가 변경될 때만 동작
불필요한 리렌더링을 방지하여 성능 최적화 가능
4️⃣ Zustand와 Next.js에서 서버 상태 관리
Next.js에서 Zustand는 서버 상태와 클라이언트 상태를 함께 관리할 수 있다.
import create from 'zustand';
// Zustand 스토어 생성
const useStore = create((set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));
export default function ThemeToggle() {
const { theme, toggleTheme } = useStore();
return (
<div>
<p>현재 테마: {theme}</p>
<button onClick={toggleTheme}>테마 변경</button>
</div>
);
}
전역적으로 다크모드 상태 관리 가능
4. Zustand의 단점
단점
- 새로고침 후 상태 유지 안 됨 → (해결법: persist 사용)
- 복잡한 상태 관리에는 Redux가 더 적합
- 서버 상태 관리에는 React Query와 함께 사용하는 것이 좋음
5. Zustand 상태 유지 (persist 사용)
Zustand의 persist 미들웨어를 사용하면 새로고침 후에도 상태를 유지 가능.
설치
npm install zustand middleware
예제
import create from 'zustand';
import { persist } from 'zustand/middleware';
// Zustand 상태 유지 설정
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' }
)
);
export default function Counter() {
const { count, increment } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}
새로고침해도 count 값이 유지됨!
3) Jotai (Recoil-like Atom 상태 관리)
Zustand처럼 간단하지만, Atom으로 세밀한 상태 관리 가능(Atomic State Management (원자 단위 상태 관리))
✔ 장점
상태를 Atom 단위로 관리 → 불필요한 리렌더링 방지
비동기 async/await 상태 지원
❌ 단점
초반 학습 필요
예제
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
export function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Jotai
Jotai는 가볍고 직관적인 원자(Atom) 기반 상태 관리 라이브러리로,
✔ Recoil과 유사한 방식으로 동작하면서도
✔ Zustand보다 더 세밀한 상태 분할이 가능하다.
1. Jotai 기본 사용법
Jotai의 핵심 개념은 Atom(원자) 단위 상태 관리이다.
설치
npm install jotai
1️⃣ 기본적인 상태 관리 (Counter 예제)
import { atom, useAtom } from 'jotai';
// Atom 생성
const countAtom = atom(0);
export default function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Zustand처럼 간단하지만, 상태를 원자(Atom) 단위로 분리 가능
불필요한 리렌더링 방지 (countAtom이 변경될 때만 관련 컴포넌트 리렌더링)
2. Jotai의 주요 기능
1️⃣ 여러 개의 Atom 상태 관리
const countAtom = atom(0);
const userAtom = atom('John');
export default function MultiStateComponent() {
const [count, setCount] = useAtom(countAtom);
const [user, setUser] = useAtom(userAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<p>User: {user}</p>
<button onClick={() => setUser('Alice')}>Change User</button>
</div>
);
}
Atom을 개별적으로 분리하여 관리 가능
Redux처럼 Store에 모든 상태를 저장할 필요 없음
2️⃣ 파생된 상태 (derived atoms)
Jotai에서는 기존 Atom을 기반으로 새로운 Atom을 생성 가능하다.
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
export default function DerivedStateComponent() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double Count: {doubleCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
countAtom이 변경되면 doubleCountAtom도 자동 업데이트
불필요한 계산을 방지하여 성능 최적화 가능
3️⃣ 비동기 상태 (async/await 지원)
Jotai는 비동기 상태(async/await)를 자연스럽게 처리할 수 있다.
const userAtom = atom(async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
return res.json();
});
export default function AsyncDataComponent() {
const [user] = useAtom(userAtom);
return user ? <p>User: {user.name}</p> : <p>Loading...</p>;
}
비동기 API Fetching을 Atom으로 저장 가능
Redux에서 redux-thunk 없이 간단하게 비동기 처리 가능
4️⃣ Jotai와 Zustand 비교 (Zustand보다 정교한 상태 관리)
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// Jotai
const countAtom = atom(0);
Zustand는 한 Store에서 상태를 관리하지만, Jotai는 개별 Atom으로 분리 가능
Jotai는 상태 변경이 필요한 컴포넌트만 리렌더링하여 성능 최적화 가능
3. Jotai + Next.js에서 사용하기
Jotai는 Next.js에서도 문제없이 동작한다.
1️⃣ Next.js에서 useAtom 사용
import { atom, useAtom } from 'jotai';
const themeAtom = atom('light');
export default function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<div>
<p>현재 테마: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경
</button>
</div>
);
}
Next.js에서도 전역 상태 관리 가능
2️⃣ Next.js 서버 상태 관리 (SSR & Hydration)
Jotai는 Next.js의 서버 상태를 Hydration할 수 있음.
import { atomWithStorage } from 'jotai/utils';
const themeAtom = atomWithStorage('theme', 'light'); // 로컬 스토리지 활용
export default function ThemeComponent() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<div>
<p>현재 테마: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경
</button>
</div>
);
}
✅ 새로고침해도 상태 유지 (localStorage 저장)
✅ Next.js와 완벽한 호환 가능
4) Redux Toolkit (전통적인 Redux 스타일)
대규모 프로젝트에는 적합
작은 프로젝트에서는 불필요하게 복잡
✔ 장점
복잡한 상태 관리 가능 (대규모 앱에 적합)
미들웨어, 비동기 지원
❌ 단점
보일러플레이트 코드 많음
학습 비용 높음
예제
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useDispatch, useSelector } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: { increment: (state) => state + 1 },
});
const store = configureStore({ reducer: counterSlice.reducer });
export function Counter() {
const count = useSelector((state) => state);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(counterSlice.actions.increment())}>+</button>
</div>
);
}
export function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
5) Nuqs (URL 기반 상태 관리)
✔ 장점
URL을 상태로 사용 → 새로고침해도 유지
뒤로 가기 (history.back()) 지원
SEO 친화적
❌ 단점
일반적인 전역 상태 관리에는 적합하지 않음
Next에서만 사용 가능
예제
import { useQueryState } from 'nuqs';
export default function SearchComponent() {
const [search, setSearch] = useQueryState('search', { defaultValue: '' });
return (
<input
value={search ?? ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색어 입력..."
/>
);
}
검색 필터, 페이지네이션 등에 유용
Nuqs
useQueryState는 Next.js & React에서 URL의 쿼리 파라미터를 상태처럼 다룰 수 있는 라이브러리입니다.
이를 활용하면 SEO 친화적인 URL 기반 상태 관리가 가능하며,
뒤로 가기/앞으로 가기 버튼 지원, 새로고침 후에도 상태 유지 등의 기능을 제공합니다.
왜 useQueryState를 사용할까?
- 쿼리 스트링을 상태로 활용 -> 새로고침 후에도 상태 유지
- history.back() 지원
- 자동으로 상태 가져오기
1. useQueryState 기본 사용법
설치
npm install nuqs
Next.js layout.tsx 설정 (필수)
Nuqs는 Next.js에서 NuqsAdapter로 감싸야 정상 작동합니다.
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { ReactNode } from 'react'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
)
}
설정 완료 후, 쿼리 파라미터를 상태처럼 사용할 준비 완료!
2. useQueryState로 URL 값 추출 및 설정
1️⃣ 쿼리 파라미터 가져오기 (기본 예제)
import { useQueryState } from 'nuqs'
export default function SearchPage() {
const [search, setSearch] = useQueryState('search', { defaultValue: '' });
return (
<div>
<input
type="text"
value={search ?? ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색어 입력..."
/>
<p>현재 검색어: {search}</p>
</div>
);
}
✔ search 상태는 URL의 ?search= 값을 기반으로 자동 설정됨
✔ 입력값 변경 시, URL도 ?search=입력값으로 업데이트됨
✔ 뒤로 가기 버튼 사용 가능 (history.back() 지원)
2️⃣ URL에서 숫자 값 추출 (parse 사용)
URL에서 숫자 값을 가져올 때는 parse를 활용하면 더욱 안전함.
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
parse: (val) => Number(val) || 1, // 숫자로 변환
});
✔ URL이 ?page=5라면, page는 5
✔ URL이 ?page=abc라면, page는 1 (기본값)
✔ setPage(2)를 호출하면, URL이 ?page=2로 변경됨
3️⃣ 여러 개의 값 한 번에 가져오기 (useQueryStates)
import { useQueryStates } from 'nuqs'
export default function MultiStatePage() {
const [state, setState] = useQueryStates({
search: { defaultValue: '' },
page: { defaultValue: 1, parse: (v) => Number(v) || 1 },
});
return (
<div>
<input
type="text"
value={state.search}
onChange={(e) => setState({ search: e.target.value })}
placeholder="검색어 입력..."
/>
<button onClick={() => setState({ page: state.page + 1 })}>
다음 페이지
</button>
<p>검색어: {state.search}</p>
<p>페이지: {state.page}</p>
</div>
);
}
✔ search, page 상태가 각각 URL 쿼리스트링으로 동기화됨
✔ 한 번의 setState 호출로 여러 개의 상태 업데이트 가능
3. useQueryState의 추가 기능
1️⃣ defaultValue (쿼리값이 없을 때 기본값 설정)
const [lang, setLang] = useQueryState('lang', { defaultValue: 'en' })
✔ URL에 ?lang=ko가 없으면, lang의 기본값은 'en'
2️⃣ parse (URL 값을 변환)
const [count, setCount] = useQueryState('count', {
defaultValue: 0,
parse: (v) => Number(v) || 0, // 숫자로 변환
});
✔ ?count=10 → count는 10
✔ ?count=abc → count는 0
3️⃣ history 옵션 (URL 변경 방식 선택)
const [mode, setMode] = useQueryState('mode', {
history: 'replace', // 기존 URL 덮어쓰기
});
✔ push (기본값) → 브라우저 히스토리에 새로운 기록 추가
✔ replace → 기존 URL을 덮어쓰기
✔ skip → URL 변경 없이 상태만 변경
4. 활용 예제
1️⃣ 페이지네이션 (Next.js)
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
parse: (v) => Number(v) || 1,
});
return (
<div>
<button onClick={() => setPage(page - 1)} disabled={page <= 1}>이전</button>
<span>현재 페이지: {page}</span>
<button onClick={() => setPage(page + 1)}>다음</button>
</div>
);
✔ URL에 ?page=2 반영 → /current-url?page=2
✔ 뒤로 가기 버튼으로 페이지 이동 가능
2️⃣ 다크모드 테마 토글
const [theme, setTheme] = useQueryState('theme', {
defaultValue: 'light',
});
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경 ({theme})
</button>
);
✔ URL에 ?theme=dark 저장
✔ 뒤로 가기 버튼으로 테마 변경 가능
3️⃣ 날짜 필터 적용
const [date, setDate] = useQueryState('date', {
defaultValue: new Date().toISOString().split('T')[0],
});
return (
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
);
✔ URL에 ?date=YYYY-MM-DD 저장
✔ 뒤로 가기 버튼으로 날짜 변경 가능
React에서는 Nuqs 대신 URLSearchParams를 사용
import { useLocation } from 'react-router-dom';
const location = useLocation();
const params = new URLSearchParams(location.search);
const token = params.get('token');
// 다른 예
<Link to="/auth?page=signin">Sign in</Link>
// 위처럼 파라미터를 보내고 아래처럼 받으면 된다.
const query = new URLSearchParams(window.location.search);
const page = query.get('page');'JavaScript > Next' 카테고리의 다른 글
| tRPC, Next.js, TanStack Query 설정 가이드 (0) | 2025.09.11 |
|---|---|
| Prismic (0) | 2025.03.06 |
| Upstash (1) | 2025.03.05 |
| Middleware 미들웨어 사용법 (0) | 2025.03.05 |
| Drizzle ORM (0) | 2025.02.12 |