본문으로 건너뛰기

🎛️ 상태관리란?

📖 정의

**상태(State)**는 애플리케이션의 데이터와 UI 상태를 의미하며, 상태관리는 이러한 상태를 효율적으로 저장, 업데이트, 공유하는 방법입니다. React에서 여러 컴포넌트가 같은 데이터를 사용할 때, 상태관리 라이브러리를 통해 복잡성을 줄이고 코드를 깔끔하게 유지할 수 있습니다.

🎯 비유로 이해하기

은행 계좌 비유

상태관리를 은행 시스템에 비유하면:

개인 지갑 (Component State)
├─ 본인만 사용
├─ 간단하고 빠름
└─ 다른 사람과 공유 불가

가족 공동 계좌 (Context API)
├─ 가족 구성원 모두 접근
├─ 설정 간단
└─ 가족이 많아지면 복잡

은행 시스템 (Redux/Zustand)
├─ 중앙 집중식 관리
├─ 모든 거래 기록
├─ 복잡하지만 체계적
└─ 감사(audit) 가능

예시:
사용자 로그인 정보
├─ 헤더: 사용자 이름 표시
├─ 사이드바: 프로필 사진
├─ 설정: 이메일 정보
└─ 푸터: 로그아웃 버튼

모든 컴포넌트가 같은 사용자 정보 필요
→ 상태관리로 한 곳에서 관리!

방송국 비유

지역 방송 (Local State)
- 한 컴포넌트 내에서만
- useState 사용
예: 모달 열림/닫힘 상태

중앙 방송 (Global State)
- 전체 앱에서 접근
- Redux/Zustand 사용
예: 로그인 사용자 정보

뉴스 속보 (State Updates)
- 중앙에서 발송
- 모든 구독자에게 전달
- 자동 UI 업데이트

리모컨 (Actions)
- 채널 변경 요청
- 볼륨 조절 요청
- 명확한 인터페이스

도서관 비유

책상 위 메모 (Local State)
- 개인 작업 공간
- 빠르고 간단
- 다른 사람 못 봄

도서관 게시판 (Context API)
- 같은 층 이용자 공유
- 중간 규모
- 설정 쉬움

중앙 데이터베이스 (Redux)
- 전체 도서관 시스템
- 모든 책 위치 추적
- 대출 기록 관리
- 체계적이지만 복잡

실시간 알림 (Reactive State)
- 책 반납되면 자동 알림
- 구독자에게만 전송
- Recoil/Jotai 방식

⚙️ 작동 원리

1. Props Drilling 문제

// ❌ Props Drilling (지옥의 계단)

// App.js
function App() {
const [user, setUser] = useState({ name: '김철수', email: 'kim@example.com' });

return <Layout user={user} setUser={setUser} />;
}

// Layout.js
function Layout({ user, setUser }) {
return (
<div>
<Header user={user} setUser={setUser} />
<Sidebar user={user} />
<Main user={user} setUser={setUser} />
</div>
);
}

// Header.js
function Header({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}

// UserMenu.js
function UserMenu({ user, setUser }) {
return <ProfileButton user={user} setUser={setUser} />;
}

// ProfileButton.js
function ProfileButton({ user, setUser }) {
// 드디어 사용!
return (
<button onClick={() => setUser({ ...user, name: '이영희' })}>
{user.name}
</button>
);
}

// 문제점:
// 1. user를 4단계나 전달 (App → Layout → Header → UserMenu → ProfileButton)
// 2. 중간 컴포넌트들이 불필요하게 props 받음
// 3. props 추가/수정 시 모든 컴포넌트 수정
// 4. 코드 복잡도 증가
// 5. 유지보수 어려움

2. 상태관리의 해결책

// ✅ 상태관리 라이브러리 사용

// store.js (중앙 저장소)
const store = {
user: { name: '김철수', email: 'kim@example.com' }
};

// App.js
function App() {
return (
<div>
<Header />
<Sidebar />
<Main />
</div>
);
// props 전달 필요 없음!
}

// ProfileButton.js
function ProfileButton() {
// 필요한 컴포넌트만 직접 접근
const user = useStore(state => state.user);
const updateUser = useStore(state => state.updateUser);

return (
<button onClick={() => updateUser({ name: '이영희' })}>
{user.name}
</button>
);
}

// 장점:
// 1. props 전달 불필요
// 2. 중간 컴포넌트 영향 없음
// 3. 코드 간결
// 4. 유지보수 쉬움
// 5. 성능 최적화 가능

3. 단방향 데이터 흐름

// 상태관리의 기본 원칙

┌─────────────────────────────────────┐
Store (저장소)
{ count: 0, user: {...}, ... }
└─────────────────────────────────────┘
구독(subscribe)

┌─────────────────────────────────────┐
Component (컴포넌트)
UI 렌더링: <div>{count}</div>
└─────────────────────────────────────┘
이벤트 (: 버튼 클릭)

┌─────────────────────────────────────┐
Action (액션)
{ type: 'INCREMENT' }
└─────────────────────────────────────┘
전달(dispatch)

┌─────────────────────────────────────┐
Reducer (리듀서)
│ count = count + 1
└─────────────────────────────────────┘
↓ 상태 업데이트

다시 Store로

// 특징:
// 1. 예측 가능: 같은 입력 → 같은 출력
// 2. 추적 가능: 모든 변경 기록
// 3. 디버깅 쉬움: 시간 여행 가능
// 4. 테스트 용이: 순수 함수

💡 실제 예시

Context API (기본)

// 1. Context 생성
import { createContext, useContext, useState } from 'react';

const UserContext = createContext();

// 2. Provider 컴포넌트
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '김철수',
email: 'kim@example.com',
isLoggedIn: false
});

const login = (email, password) => {
// API 호출
setUser({
name: '김철수',
email: email,
isLoggedIn: true
});
};

const logout = () => {
setUser({
name: '',
email: '',
isLoggedIn: false
});
};

return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}

// 3. Custom Hook
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}

// 4. App에서 Provider로 감싸기
function App() {
return (
<UserProvider>
<Header />
<Main />
<Footer />
</UserProvider>
);
}

// 5. 사용
function Header() {
const { user, logout } = useUser();

return (
<header>
<h1>안녕하세요, {user.name}</h1>
{user.isLoggedIn && (
<button onClick={logout}>로그아웃</button>
)}
</header>
);
}

function LoginForm() {
const { login } = useUser();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">로그인</button>
</form>
);
}

// Context API의 장단점:
// 장점:
// - 추가 라이브러리 불필요
// - 간단한 설정
// - React 내장 기능

// 단점:
// - 성능 최적화 어려움
// - Provider 중첩 지옥
// - DevTools 없음

Redux (복잡하지만 강력)

// 1. Store 설정
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';

// Slice 생성 (상태 + 리듀서)
const userSlice = createSlice({
name: 'user',
initialState: {
name: '',
email: '',
isLoggedIn: false,
loading: false,
error: null
},
reducers: {
loginStart: (state) => {
state.loading = true;
state.error = null;
},
loginSuccess: (state, action) => {
state.name = action.payload.name;
state.email = action.payload.email;
state.isLoggedIn = true;
state.loading = false;
},
loginFailure: (state, action) => {
state.loading = false;
state.error = action.payload;
},
logout: (state) => {
state.name = '';
state.email = '';
state.isLoggedIn = false;
}
}
});

// Actions export
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;

// Thunk (비동기 액션)
export const loginAsync = (email, password) => async (dispatch) => {
dispatch(loginStart());
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
dispatch(loginSuccess(data));
} catch (error) {
dispatch(loginFailure(error.message));
}
};

// Store 생성
const store = configureStore({
reducer: {
user: userSlice.reducer
}
});

export default store;

// 2. App에 Provider 연결
// index.js
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

// 3. 컴포넌트에서 사용
// LoginForm.js
import { useDispatch, useSelector } from 'react-redux';
import { loginAsync } from './store';

function LoginForm() {
const dispatch = useDispatch();
const { loading, error } = useSelector(state => state.user);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
dispatch(loginAsync(email, password));
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? '로그인 중...' : '로그인'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}

// Header.js
import { useDispatch, useSelector } from 'react-redux';
import { logout } from './store';

function Header() {
const dispatch = useDispatch();
const { name, isLoggedIn } = useSelector(state => state.user);

return (
<header>
{isLoggedIn && (
<>
<span>안녕하세요, {name}</span>
<button onClick={() => dispatch(logout())}>
로그아웃
</button>
</>
)}
</header>
);
}

// Redux의 장단점:
// 장점:
// - 강력한 DevTools (시간 여행 디버깅)
// - 미들웨어 (로깅, 비동기 처리)
// - 거대한 생태계
// - 예측 가능한 상태 관리
// - 엄격한 구조

// 단점:
// - 보일러플레이트 많음
// - 학습 곡선 높음
// - 간단한 앱에는 과함

Zustand (간단하고 현대적)

// 1. Store 생성
// store.js
import create from 'zustand';

const useStore = create((set, get) => ({
// 상태
user: {
name: '',
email: '',
isLoggedIn: false
},
loading: false,
error: null,

// 액션
login: async (email, password) => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
set({
user: {
name: data.name,
email: data.email,
isLoggedIn: true
},
loading: false
});
} catch (error) {
set({ error: error.message, loading: false });
}
},

logout: () => {
set({
user: {
name: '',
email: '',
isLoggedIn: false
}
});
},

// 파생 상태 (computed)
getFullName: () => {
const { user } = get();
return user.name || '게스트';
}
}));

export default useStore;

// 2. 컴포넌트에서 사용 (Provider 불필요!)
// LoginForm.js
import useStore from './store';

function LoginForm() {
const { login, loading, error } = useStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button disabled={loading}>
{loading ? '로그인 중...' : '로그인'}
</button>
{error && <p>{error}</p>}
</form>
);
}

// Header.js
import useStore from './store';

function Header() {
// 필요한 상태만 선택 (성능 최적화)
const user = useStore(state => state.user);
const logout = useStore(state => state.logout);

return (
<header>
{user.isLoggedIn && (
<>
<span>안녕하세요, {user.name}</span>
<button onClick={logout}>로그아웃</button>
</>
)}
</header>
);
}

// 3. 미들웨어 사용
import create from 'zustand';
import { persist } from 'zustand/middleware';

// localStorage에 자동 저장
const useStore = create(
persist(
(set) => ({
user: { name: '', email: '', isLoggedIn: false },
login: async (email, password) => {
// ...
}
}),
{
name: 'user-storage' // localStorage key
}
)
);

// Zustand의 장단점:
// 장점:
// - 매우 간단한 API
// - Provider 불필요
// - TypeScript 완벽 지원
// - 작은 번들 크기 (1KB)
// - Redux DevTools 지원
// - 미들웨어 지원

// 단점:
// - 생태계 작음
// - 복잡한 앱에서는?
// - 공식 문서 부족

Recoil (React 친화적)

// 1. Atoms (상태 단위)
// atoms.js
import { atom, selector } from 'recoil';

// Atom: 상태의 조각
export const userState = atom({
key: 'userState',
default: {
name: '',
email: '',
isLoggedIn: false
}
});

export const loadingState = atom({
key: 'loadingState',
default: false
});

// Selector: 파생 상태
export const userNameState = selector({
key: 'userNameState',
get: ({ get }) => {
const user = get(userState);
return user.name || '게스트';
}
});

// 비동기 Selector
export const userDataQuery = selector({
key: 'userDataQuery',
get: async ({ get }) => {
const response = await fetch('/api/user');
return response.json();
}
});

// 2. App에 RecoilRoot 추가
// App.js
import { RecoilRoot } from 'recoil';

function App() {
return (
<RecoilRoot>
<Header />
<Main />
<Footer />
</RecoilRoot>
);
}

// 3. 컴포넌트에서 사용
// LoginForm.js
import { useRecoilState, useSetRecoilState } from 'recoil';
import { userState, loadingState } from './atoms';

function LoginForm() {
const [user, setUser] = useRecoilState(userState);
const setLoading = useSetRecoilState(loadingState);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser({
name: data.name,
email: data.email,
isLoggedIn: true
});
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleLogin}>
{/* ... */}
</form>
);
}

// Header.js
import { useRecoilValue } from 'recoil';
import { userNameState } from './atoms';

function Header() {
const userName = useRecoilValue(userNameState);

return (
<header>
<span>안녕하세요, {userName}</span>
</header>
);
}

// 4. 비동기 데이터
import { useRecoilValueLoadable } from 'recoil';
import { userDataQuery } from './atoms';

function UserProfile() {
const userLoadable = useRecoilValueLoadable(userDataQuery);

switch (userLoadable.state) {
case 'loading':
return <div>로딩 중...</div>;
case 'hasError':
return <div>에러: {userLoadable.contents.message}</div>;
case 'hasValue':
return <div>{userLoadable.contents.name}</div>;
}
}

// Recoil의 장단점:
// 장점:
// - React Hooks 스타일
// - 비동기 처리 간편
// - 파생 상태 자동 계산
// - Concurrent Mode 지원
// - Facebook 제작

// 단점:
// - 아직 실험 단계
// - 안정성 우려
// - 생태계 작음
// - DevTools 부족

비교 예시: 쇼핑카트

// Context API 버전
const CartContext = createContext();

function CartProvider({ children }) {
const [items, setItems] = useState([]);

const addItem = (item) => {
setItems([...items, item]);
};

const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};

const total = items.reduce((sum, item) => sum + item.price, 0);

return (
<CartContext.Provider value={{ items, addItem, removeItem, total }}>
{children}
</CartContext.Provider>
);
}

// Redux 버전
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});

// Zustand 버전
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price, 0);
}
}));

// Recoil 버전
const cartState = atom({
key: 'cartState',
default: []
});

const cartTotalSelector = selector({
key: 'cartTotalSelector',
get: ({ get }) => {
const cart = get(cartState);
return cart.reduce((sum, item) => sum + item.price, 0);
}
});

// 사용
function ShoppingCart() {
// Context
const { items, removeItem, total } = useContext(CartContext);

// Redux
const items = useSelector(state => state.cart.items);
const dispatch = useDispatch();

// Zustand
const { items, removeItem, total } = useCartStore();

// Recoil
const [items, setItems] = useRecoilState(cartState);
const total = useRecoilValue(cartTotalSelector);

return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name} - {item.price}
<button onClick={() => removeItem(item.id)}>삭제</button>
</div>
))}
<p>총합: {total}</p>
</div>
);
}

성능 최적화

// 1. Context API 성능 문제
function BadContext() {
const [user, setUser] = useState({ name: '', age: 0 });
const [theme, setTheme] = useState('light');

// 문제: theme만 바뀌어도 user 사용하는 컴포넌트 모두 리렌더링
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}

// ✅ 해결: Context 분리
function GoodContext() {
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}

// 2. Redux 선택자 최적화
// ❌ 나쁜 예
function BadComponent() {
const state = useSelector(state => state); // 전체 상태!
return <div>{state.user.name}</div>;
}

// ✅ 좋은 예
function GoodComponent() {
const userName = useSelector(state => state.user.name); // 필요한 것만
return <div>{userName}</div>;
}

// 3. Zustand 선택자 최적화
// ❌ 나쁜 예
function BadComponent() {
const store = useStore(); // 전체 스토어
return <div>{store.user.name}</div>;
}

// ✅ 좋은 예
function GoodComponent() {
const userName = useStore(state => state.user.name); // 필요한 것만
return <div>{userName}</div>;
}

// 4. Recoil atomFamily (동적 상태)
const userItemState = atomFamily({
key: 'UserItem',
default: id => fetch(`/api/users/${id}`).then(r => r.json())
});

function UserProfile({ userId }) {
const user = useRecoilValue(userItemState(userId));
return <div>{user.name}</div>;
}

// 5. 메모이제이션
import { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ value }) {
console.log('렌더링:', value);
return <div>{value}</div>;
});

// value가 같으면 재렌더링 안 함!

🤔 자주 묻는 질문

Q1. 어떤 상태관리 라이브러리를 선택해야 하나요?

A: 프로젝트 규모와 요구사항에 따라 다릅니다:

// 선택 가이드

// Context API - 간단한 앱
const contextUseCases = {
적합한 경우: [
'작은 프로젝트',
'상태 공유 적음',
'추가 라이브러리 원치 않음',
'학습 목적'
],
예시: [
'테마 (다크/라이트 모드)',
'언어 설정',
'간단한 인증 상태',
'모달 상태'
],
장점: '별도 설치 불필요',
단점: '성능 최적화 어려움'
};

// Redux - 대규모 복잡한 앱
const reduxUseCases = {
적합한 경우: [
'대규모 프로젝트',
'복잡한 상태 로직',
'시간 여행 디버깅 필요',
'엄격한 구조 원함',
'팀 프로젝트'
],
예시: [
'기업용 대시보드',
'복잡한 e커머스',
'금융 애플리케이션',
'SaaS 플랫폼'
],
장점: '강력한 DevTools, 생태계',
단점: '학습 곡선, 보일러플레이트'
};

// Zustand - 중간 규모, 현대적
const zustandUseCases = {
적합한 경우: [
'중간 규모 프로젝트',
'간단한 API 선호',
'빠른 개발',
'TypeScript 사용'
],
예시: [
'일반적인 웹 앱',
'MVP 개발',
'스타트업 프로젝트',
'프로토타입'
],
장점: '간단함, 작은 번들',
단점: '생태계 작음'
};

// Recoil - React 중심, 비동기
const recoilUseCases = {
적합한 경우: [
'React 스타일 선호',
'비동기 데이터 많음',
'파생 상태 복잡',
'실험적 프로젝트'
],
예시: [
'실시간 협업 도구',
'데이터 시각화',
'복잡한 폼',
'GraphQL 앱'
],
장점: 'React스러움, 비동기 간편',
단점: '아직 실험 단계'
};

// Jotai - Recoil 대안
const jotaiUseCases = {
적합한 경우: [
'Recoil 원하지만 안정성 필요',
'원자 단위 상태',
'작은 번들 크기'
],
장점: '간단, 안정적',
단점: '생태계 작음'
};

// 의사결정 트리
function chooseStateManagement(project) {
if (project.size === 'small') {
return 'Context API';
}

if (project.size === 'large' && project.needsStructure) {
return 'Redux Toolkit';
}

if (project.needsSimplicity && project.size === 'medium') {
return 'Zustand';
}

if (project.hasAsyncData && project.likesReactStyle) {
return 'Recoil or Jotai';
}

return 'Zustand'; // 대부분의 경우 무난
}

Q2. 모든 상태를 전역으로 관리해야 하나요?

A: 아니요, 필요한 것만 전역으로 관리하세요:

// 상태 분류

// 1. Local State (컴포넌트 내부)
function SearchBox() {
const [query, setQuery] = useState(''); // 로컬 상태
const [isFocused, setIsFocused] = useState(false);

// 다른 컴포넌트가 필요 없는 상태
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
);
}

// 2. Lifted State (부모로 끌어올림)
function Parent() {
const [count, setCount] = useState(0); // 여러 자식이 공유

return (
<div>
<ChildA count={count} />
<ChildB setCount={setCount} />
</div>
);
}

// 3. Global State (전역 상태)
// - 많은 컴포넌트가 접근
// - 앱 전체에 영향
const globalStates = {
user: '로그인 사용자 정보',
theme: '다크/라이트 모드',
language: '언어 설정',
cart: '쇼핑카트',
notifications: '알림 목록'
};

// 4. Server State (서버 데이터)
// React Query, SWR 사용 권장
function UserProfile() {
const { data, error, isLoading } = useQuery('user', fetchUser);

if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>에러!</div>;
return <div>{data.name}</div>;
}

// 원칙:
// - 기본은 Local State
// - 2-3개 컴포넌트 공유 → Lifted State
// - 많은 컴포넌트 공유 → Global State
// - API 데이터 → Server State (React Query/SWR)

Q3. Redux Toolkit과 Redux의 차이는?

A: Redux Toolkit은 Redux를 쉽게 사용하도록 만든 공식 도구입니다:

// 기존 Redux (복잡함)
// actionTypes.js
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';

// actions.js
function increment() {
return { type: INCREMENT };
}

function decrement() {
return { type: DECREMENT };
}

// reducer.js
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, value: state.value + 1 };
case DECREMENT:
return { ...state, value: state.value - 1 };
default:
return state;
}
}

// store.js
import { createStore } from 'redux';
const store = createStore(counterReducer);

// Redux Toolkit (간단함)
// counterSlice.js
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Immer로 불변성 자동 처리!
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});

// Redux Toolkit의 장점:
// 1. 보일러플레이트 대폭 감소
// 2. Immer로 불변성 자동 처리
// 3. Redux DevTools 자동 설정
// 4. createAsyncThunk로 비동기 간편
// 5. TypeScript 지원 우수

// 비동기 처리 비교
// 기존 Redux (redux-thunk)
function fetchUser(id) {
return async (dispatch) => {
dispatch({ type: 'user/fetchStart' });
try {
const user = await api.fetchUser(id);
dispatch({ type: 'user/fetchSuccess', payload: user });
} catch (error) {
dispatch({ type: 'user/fetchFailure', payload: error });
}
};
}

// Redux Toolkit (createAsyncThunk)
const fetchUser = createAsyncThunk(
'user/fetch',
async (id) => {
const user = await api.fetchUser(id);
return user;
}
);

const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});

// 결론: 새 프로젝트는 Redux Toolkit 사용!

Q4. React Query/SWR과 상태관리 라이브러리의 차이는?

A: 서로 다른 목적으로 사용됩니다:

// 상태관리 라이브러리 (Redux, Zustand 등)
// 목적: 클라이언트 상태 관리
const clientState = {
user: { name: '김철수', isLoggedIn: true }, // 로그인 상태
theme: 'dark', // UI 설정
cart: [{ id: 1, name: '상품' }], // 쇼핑카트
modal: { isOpen: false } // UI 상태
};

// React Query/SWR
// 목적: 서버 상태 관리 (캐싱, 동기화, 재검증)
import { useQuery, useMutation } from 'react-query';

function UserProfile() {
// 자동 캐싱, 재검증, 로딩 상태 관리
const { data, isLoading, error } = useQuery('user', fetchUser);

const mutation = useMutation(updateUser, {
onSuccess: () => {
// 캐시 무효화
queryClient.invalidateQueries('user');
}
});

return (
<div>
{isLoading && <div>로딩 중...</div>}
{error && <div>에러: {error.message}</div>}
{data && <div>{data.name}</div>}
<button onClick={() => mutation.mutate({ name: '새이름' })}>
수정
</button>
</div>
);
}

// 함께 사용하기 (권장!)
// store.js (Zustand)
const useStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
// 클라이언트 상태만
}));

// UserProfile.js
function UserProfile() {
// 서버 상태: React Query
const { data: user } = useQuery('user', fetchUser);

// 클라이언트 상태: Zustand
const theme = useStore(state => state.theme);

return (
<div className={theme}>
{user.name}
</div>
);
}

// 비교:
// Redux/Zustand:
// - 클라이언트 상태
// - 직접 관리
// - 동기적

// React Query/SWR:
// - 서버 상태
// - 자동 캐싱/동기화
// - 비동기적
// - 백그라운드 재검증
// - 낙관적 업데이트

// 결론:
// 둘 다 사용하면 최고!
// - React Query: API 데이터
// - Zustand: UI 상태, 설정

Q5. 상태관리 없이 개발할 수 있나요?

A: 네, 간단한 앱은 useState와 props로 충분합니다:

// 상태관리 라이브러리 없이 개발

// 1. 작은 앱
function SmallApp() {
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);

// props로 전달
return (
<div>
<Header user={user} />
<Counter count={count} setCount={setCount} />
</div>
);
}

// 2. Custom Hooks로 로직 분리
function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

const login = async (email, password) => {
setLoading(true);
const user = await api.login(email, password);
setUser(user);
setLoading(false);
};

const logout = () => {
setUser(null);
};

return { user, loading, login, logout };
}

function App() {
const { user, login, logout } = useUser();

return (
<div>
<Header user={user} logout={logout} />
<LoginForm login={login} />
</div>
);
}

// 3. React Query만 사용
function App() {
const { data: user } = useQuery('user', fetchUser);
const { data: posts } = useQuery('posts', fetchPosts);

return (
<div>
<Header user={user} />
<PostList posts={posts} />
</div>
);
}

// 언제 상태관리 라이브러리가 필요할까?
const needsStateManagement = {
signs: [
'props 전달이 3단계 이상',
'여러 컴포넌트가 같은 상태 사용',
'상태 업데이트 로직이 복잡',
'전역 설정 필요 (테마, 언어)',
'디버깅 어려움'
]
};

// 결론:
// - 시작은 간단하게 (useState + props)
// - 복잡해지면 Context API
// - 더 복잡해지면 Zustand/Redux
// - API 데이터는 React Query/SWR

🎓 다음 단계

상태관리를 이해했다면, 다음을 학습해보세요:

  1. React란? (문서 작성 예정) - React 기초 복습
  2. Virtual DOM이란? - React 렌더링 원리
  3. 웹 성능 최적화 (문서 작성 예정) - 상태관리 성능 최적화

🎬 마무리

상태관리는 현대 웹 개발의 핵심입니다:

  • Context API: 간단한 앱, React 내장
  • Redux: 대규모 앱, 강력한 도구
  • Zustand: 중간 규모, 간단한 API
  • Recoil: React 스타일, 비동기 편리

프로젝트에 맞는 도구를 선택하고, 꼭 필요한 상태만 전역으로 관리하세요!