React 상태 관리 전략 비교: useState부터 Zustand까지
·8분 읽기
React 프로젝트가 커지면 가장 먼저 고민하게 되는 것이 상태 관리입니다. "useState만으로 충분할까?", "Redux를 써야 할까?", "요즘 Zustand가 낫다던데?" 이 글에서 각 방식의 장단점을 정리하고 선택 기준을 제시합니다.
useState — 로컬 상태#
컴포넌트 내부에서만 사용하는 상태는 useState로 충분합니다.
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
언제 쓸까?
- 폼 입력값, 모달 열림/닫힘, 로딩 상태 등 컴포넌트 하나에서만 쓰는 상태
Props Drilling 문제#
깊이 중첩된 컴포넌트에 상태를 전달할 때 발생합니다.
// ❌ 모든 중간 컴포넌트가 user를 전달해야 함
function App() {
const [user, setUser] = useState(null)
return <Layout user={user} />
}
function Layout({ user }) {
return <Sidebar user={user} />
}
function Sidebar({ user }) {
return <UserCard user={user} />
}
function UserCard({ user }) {
return <div>{user.name}</div> // 실제 사용은 여기서만
}
이 문제를 해결하는 방법이 Context API와 외부 상태 관리 라이브러리입니다.
Context API — 전역 상태 (기본)#
React 내장 기능으로, 설치 없이 사용 가능합니다.
// context 생성
const UserContext = createContext<User | null>(null)
// Provider로 감싸기
function App() {
const [user, setUser] = useState<User | null>(null)
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
)
}
// 어느 자식 컴포넌트에서든 접근 가능
function UserCard() {
const user = useContext(UserContext)
return <div>{user?.name}</div>
}
Context의 단점#
Context 값이 바뀌면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링됩니다.
// ❌ theme과 user를 같은 Context에 넣으면
// theme이 바뀔 때 user만 쓰는 컴포넌트도 리렌더링
const AppContext = createContext({ theme: 'dark', user: null })
// ✅ Context를 분리해서 관리
const ThemeContext = createContext('dark')
const UserContext = createContext(null)
언제 쓸까?
- 테마, 언어 설정, 로그인 유저 정보처럼 자주 바뀌지 않는 전역 상태
Redux — 대규모 앱 상태 관리#
복잡한 상태 로직을 예측 가능하게 관리합니다.
// Redux Toolkit 사용
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value++ },
decrement: (state) => { state.value-- },
},
})
const store = configureStore({
reducer: { counter: counterSlice.reducer }
})
// 컴포넌트에서 사용
function Counter() {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
</div>
)
}
언제 쓸까?
- 복잡한 상태 로직이 많은 대규모 앱
- 상태 변경 이력 추적(Redux DevTools)이 필요한 경우
- 팀 규모가 크고 일관된 패턴이 중요한 경우
Zustand — 가볍고 직관적인 전역 상태#
최근 가장 빠르게 성장하는 상태 관리 라이브러리입니다.
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// 컴포넌트에서 사용 — Provider 불필요!
function Counter() {
const { count, increment } = useCounterStore()
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
</div>
)
}
선택적 구독으로 리렌더링 최적화#
// count가 바뀔 때만 리렌더링
const count = useCounterStore((state) => state.count)
// increment만 필요한 컴포넌트는 count가 바뀌어도 리렌더링 안 됨
const increment = useCounterStore((state) => state.increment)
언제 쓸까?
- 소중형 앱의 전역 상태
- Redux가 너무 복잡하게 느껴질 때
- 빠른 개발이 필요한 스타트업 프로젝트
라이브러리 비교#
| 기준 | Context | Redux | Zustand |
|---|---|---|---|
| 번들 크기 | 0 (내장) | ~48KB | ~1KB |
| 보일러플레이트 | 적음 | 많음 | 매우 적음 |
| 리렌더링 최적화 | 어려움 | 쉬움 | 매우 쉬움 |
| DevTools | 없음 | 강력함 | 기본 제공 |
| 학습 곡선 | 낮음 | 높음 | 낮음 |
| TypeScript 지원 | 보통 | 좋음 | 매우 좋음 |
상태 종류별 선택 가이드#
상태 종류에 따른 선택:
로컬 상태 (하나의 컴포넌트)
└─→ useState, useReducer
서버 상태 (API 데이터, 캐싱)
└─→ TanStack Query (React Query), SWR
전역 UI 상태 (테마, 모달, 알림)
└─→ 단순: Context API
복잡/자주 바뀜: Zustand
복잡한 비즈니스 로직
└─→ Redux Toolkit
마무리#
정답은 없습니다. 프로젝트 규모와 팀 상황에 맞게 선택하세요.
- 사이드 프로젝트 / 스타트업: Zustand + TanStack Query
- 대규모 엔터프라이즈: Redux Toolkit
- 간단한 설정값 공유: Context API
한 가지 덧붙이면, 상태 관리 라이브러리 선택보다 어떤 상태를 어디에 둘 것인가를 잘 설계하는 것이 더 중요합니다.
관련 포스트
개발#성능 최적화#Web Vitals#Next.js#프론트엔드
웹 성능 최적화 실전 가이드: Core Web Vitals와 최적화 기법
LCP, FID, CLS 등 Core Web Vitals의 의미와 실제 프론트엔드 성능을 개선하는 실전 기법을 정리했습니다.
·9분 읽기
개발#Docker#DevOps#컨테이너#인프라
Docker 입문 가이드: 개발자가 꼭 알아야 할 컨테이너 기초
Docker가 왜 필요한지부터 이미지, 컨테이너, Docker Compose까지 개발자 관점에서 실용적으로 정리했습니다.
·8분 읽기
개발#API#REST#GraphQL#백엔드
REST API vs GraphQL: 실무에서 뭘 선택해야 할까?
REST와 GraphQL의 핵심 차이를 이해하고, 프로젝트 상황에 따른 올바른 선택 기준을 정리했습니다.
·8분 읽기