리액트의 기본 상태 관리 훅인 useState 와 useContext 는 컴포넌트 내부 상태 관리와 전역 데이터 공유에 유용하지만, 컴포넌트 트리가 깊어지면 prop drilling 문제가 발생한다.
prop drilling 은 상위 컴포넌트의 상태를 하위 컴포넌트에 props 로 반복 전달해야 하는 상황으로, 중간 컴포넌트가 불필요하게 props 를 받게 되어 코드가 복잡해지고 유지보수가 어려워진다.
// App.jsfunction App() {const [theme, setTheme] = useState('light');return <Toolbar theme={theme} />; // 1단계 전달}// Toolbar.js (중간 컴포넌트, theme 불필요)function Toolbar({ theme }) {return <Button theme={theme} />; // 2단계 전달}// Button.js (theme 필요)function Button({ theme }) {return <button className={theme}>Click</button>;}
이처럼 Toolbar 가 theme 을 사용하지 않음에도 props 로 전달해야 하며, 계층이 깊어질수록 문제가 심화된다.
useContext 로 개선 시도와 한계
useContext 로 prop drilling 을 피할 수 있지만, 상태 업데이트 로직을 공유하려면 추가 처리가 필요하고 모든 consumer 컴포넌트가 상태 변경 시 재렌더링되어 성능 저하가 발생한다.
// ThemeContext.jsconst ThemeContext = createContext();function App() {const [theme, setTheme] = useState('light');return (
여기서 theme 변경 시 Toolbar 와 Button 모두 재렌더링되며, 대규모 앱에서는 불필요한 리렌더링이 빈번해진다.
이러한 한계로 복잡한 앱에서는 Redux Toolkit, Zustand, Jotai 같은 상태 관리 라이브러리를 고려한다. 이 라이브러리들은 선택적 업데이트와 미들웨어를 통해 성능을 최적화한다.
리액트 기본 훅 (useState, useContext) 으로 상태 변경 로직을 관리할 때, 여러 컴포넌트에서 상태를 독립적으로 업데이트하면 로직이 분산되어 전체 애플리케이션의 복잡도가 급증한다.
분산 로직 문제 예시
여러 컴포넌트가 사용자 정보를 공유한다고 가정할 때, 각 컴포넌트에서 개별 setUser 를 호출하면 동기화 오류와 중복 로직이 발생한다.
// Profile.jsconst [user, setUser] = useState({ name: '' });const updateName = (name) => setUser({ ...user, name
이 구조는 상태 불일치 (한 곳 변경 시 다른 곳 미반영) 와 디버깅 어려움을 초래하며, 앱 규모가 커질수록 유지보수가 불가능해진다.
복잡도 증가 원인
이 문제를 해결하기 위해 Zustand 나 Redux 같은 라이브러리는 중앙화된 업데이트 로직과 선택적 구독을 제공해 복잡도를 줄인다.
App 에서 하위 컴포넌트로 넘겨주는 구조라면, 상태관리 라이브러리를 사용하더라도 마찬가지이지 않을까?
이론적으로 말고 현실적으로 접근해보자. 형제 간의 상태 공유가 필요한 상황을 가정하자.
flowchart TDA["A<br/>B와 C의 부모 컴포넌트"]B["B<br/>상태를 변경하는 컴포넌트"]C["C<br/>상태 사용하는 컴포넌트"]A --> BA --> C
리액트 기본 훅으로 형제 컴포넌트 (B, C) 간 상태 공유 시 부모 (A) 에서 상태를 관리하고 props 로 전달해야 하며, B 에서 상태 변경 시 A 와 C 모두 재렌더링된다.
하지만, 상태 관리 라이브러리는 A 를 상태와 무관하게 분리하고 B, C 만 선택적으로 업데이트한다.
기본 훅 (useState + props) 예시
// A (부모)function A() {const [sharedData, setSharedData] = useState('initial');return (<div><B data={sharedData}
B 에서 업데이트 시 A 전체와 C 가 불필요하게 리렌더링되며, A 의 다른 자식들도 영향받는다.
상태 관리 라이브러리 (Zustand 예시)
// store.jsimport { create } from 'zustand';const useStore = create((set) => ({sharedData: 'initial',updateData: (newData) =>
라이브러리는 A 를 상태 로직에서 완전히 분리하고, B 변경 시 C 만 selector 기반으로 업데이트되어 성능이 최적화된다.
상태 관리 라이브러리는 단순히 전역화뿐만 아니라, 선택적 구독과 중앙화된 로직으로 컴포넌트 책임을 분리하는 것이 핵심이다.
Flux 패턴은 리액트 상태 관리 라이브러리의 시작점으로, Facebook 에서 MVC 의 양방향 데이터 흐름 문제를 해결하기 위해 개발한 단방향 데이터 흐름 아키텍처다.
Facebook 의 대규모 리액트 앱 (알림 기능 등) 에서 모델과 뷰 간 의존성 증가, 예측 불가능한 버그가 발생했다. 기존 MVC 는 뷰가 모델을 직접 수정하는 양방향 흐름으로 복잡도가 폭증했다.

이를 해결하기 위해 Action → Dispatcher → Store → View의 단방향 흐름을 제안했다. Store 가 변경 시 의존 뷰만 업데이트되어 데이터 흐름이 예측 가능해진다.

| 개념 | 설명 |
| Action | - 상태 변경을 요청하는 객체 - dispatch({ type: "INCREMENT", payload: 1 }) |
| Dispatcher | - 모든 데이터 변경 요청이 경유하는 곳 - 모든 Action을 받아 등록된 각 Store의 콜백을 호출 |
| Store | - 애플리케이션의 상태를 저장하고 변경을 관리 |
| View | - Store 를 구독하여 UI 를 업데이트 |
Context API + useReducer 로 Flux-like 패턴 구현 가능해졌으나, 보일러플레이트 문제로 Zustand, Jotai 등이 최소 Flux 원칙을 유지하며 최적화되었다. Flux 가 상태 관리 라이브러리 시대를 연 원조지만, 현재는 Redux Toolkit 이 그 정신을 이어가는 주류다.
Redux 는 2015 년 Dan Abramov 가 Facebook 의 Flux 패턴을 단순화해 개발한 라이브러리로, 대규모 MVC 앱의 양방향 데이터 흐름 문제를 해결하기 위해 등장했다. Flux 는 패턴이고 Redux 는 Flux 를 구현한 라이브러리다.
초기 리액트 앱은 MVC 패턴을 사용했으나, 모델 - 뷰 간 양방향 데이터 흐름으로 앱 규모가 커지며 상태 변경 추적이 불가능해졌다. Facebook 이 단방향 흐름의 Flux 아키텍처를 제안했으나 Dispatcher 등 복잡성으로 구현체가 다양해졌다.
Redux 는 Dispatcher 를 제거하고 단일 Store + Reducer 로 단순화했으며, Flux 의 핵심 원칙 (단방향성) 을 계승했다.

Action(dispatch) → Reducer → Store → View(재렌더링)
| 개념 | 설명 |
| Action | - 상태 변경을 설명하는 JS 객체 |
| Dispatch | - Action을 Store로 전달하는 Store 메서드 |
| Reducer | - 이전 상태와 Action을 받아 새로운 상태 반환하는 순수 함수 |
| Store | - 단일 상태 트리 보유 |
Hot Reloading이란, 코드 변경사항을 저장한 후 애플리케이션을 새로고침 없이 실시간으로 반영하는 기능을 뜻한다. Flux 패턴에서 Hot Reloading 문제가 발생하는 이유는 Store 가 상태 변환 로직과 실제 애플리케이션 상태를 동시에 담당하기 때문이다.
Flux Store 의 이중 역할 문제
Flux 의 Store 는 두 가지 책임을 혼재
// Flux Store 예시 (문제 구조)class TodoStore {constructor() {_todos = []; // 1️⃣ 실제 앱 상태 (인스턴스 변수)AppDispatcher.register(this.handleActions.bind(this)); // 2️⃣ 상태 변환 로직
코드 변경 → Store 인스턴스 전체 재생성 → _todos 상태 초기화 → 저장된 데이터 손실.
Redux 의 해결 방법
Redux 는 Store 와 Reducer 를 분리해 상태는 유지, 로직만 리로딩한다.
이 역할 분리가 Redux 를 Flux 보다 개발자 경험 (DX) 이 뛰어난 상태 관리 표준으로 만든 핵심이다.
// Redux 구조 (해결됨)const todoReducer = (state = [], action) => { // 1️⃣ 순수 로직만 (외부 파일)switch(action.type) {case 'ADD_TODO':return [
todoReducer 만 리로드 → store 의 기존 상태 보존 → 데이터 유지
Time Travel Debugging은 프로그램의 실행 과정을 기록해 두었다가, 그 기록을 앞으로·뒤로 마음대로 이동하며 디버깅할 수 있게 해 주는 기법을 말한다.
Flux Store 는 상태를 직접 수정하는 방식을 사용하기 때문에 Time Travel Debugging을 구현하기가 복잡하다. Store 가 상태뿐 아니라 히스토리와 시간 이동 로직까지 모두 관리해야 하므로, 코드 복잡도가 급격히 증가하고 메모리 사용량도 늘어나게 된다.
// 가변성 Flux Store (시간여행 불가능)class TodoStore {constructor() {this.todos = []; // 공유 배열}addTodo(todo) {this.todos
push() 로 배열 끝에 추가 → 과거 히스토리도 동일 배열 참조 → 모든 상태가 최신 값으로 오염
// 불변성 Flux Store (시간여행 가능)class TodoStore {constructor() {this._history = [{ todos: [] }];this.currentIndex = 0;
Redux 의 해결 방법
Redux 의 Time Travel Debugging은 Redux DevTools 에서 모든 액션과 상태 변화를 기록하여, 과거의 특정 시점으로 돌아가며 디버깅할 수 있도록 도와준다.
Redux 에서는 Time Travel Debugging을 가능하게 하기 위해서 상태를 불변 객체로 관리한다. 상태가 불변이면, 매번 새로운 상태가 생성되어 이전 상태들이 그대로 보존된다. 이렇게 해야 상태 히스토리를 재현할 수 있고, 과거의 특정 시점으로 정확하게 복원할 수 있다.
가변성: [{todos: [참조]}] ← [참조] ← ['a','b','c'] (모두 같은 배열!)불변성: [{todos: []}, {
기존 애플리케이션 상태를 직접 수정하지 않고, 상태를 복사한 뒤 그 복사본을 수정하는 방식으로 불변성을 유지한다. 이러한 방식은 Flux 에서도 구현할 수 있지만, 구조가 복잡해 다루기 어렵다. 반면 Redux 에서는 이를 훨씬 단순하게 처리할 수 있다.
Redux 는 Flux 구현체 중 가장 성공적이었으며, 현대 상태 관리의 표준을 세웠다. 디버깅 용이성으로 폭발적 인기를 끌었으나 action 타입, reducer 보일러플레이트가 많아졌다.
이를 해결하기 위해 2019 년 Redux Toolkit(RTK) 이 출시되어 createSlice 등으로 코드 양을 60% 줄였다.
미니멀 스토어들이 Redux(RTK) 를 어떻게 개선했는지 동일한 카운터 기능으로 예시 코드 비교해보자.
Redux Toolkit (기준)
총: 40+ 줄 + Provider 설정 + 3 개 파일
// store/counterSlice.js (10+ 파일/줄)import { createSlice } from '@reduxjs/toolkit';const counterSlice = createSlice({name: 'counter',initialState: { value: 0 },
Zustand (90% 코드 감소)
총: 8 줄 + Provider 없음 + 선택적 구독
// store.js (단일 파일 8줄)import { create } from 'zustand';const useCounterStore = create((set) => ({count: 0,increment: ()
Jotai (아토믹 방식)
총: 6 줄 + Bottom-up + 자동 최적화
// atoms.js (3줄씩 분리 가능)import { atom, useAtom } from 'jotai';const countAtom = atom(0); // 상태const incrementAtom = atom(null, (get, set
Zustand 는 “작고, 빠르고, 확장 가능하며, 불필요한 보일러플레이트가 없는 hooks 기반 상태 관리 라이브러리” 를 지향한다. 작은 번들 크기와 높은 성능을 통해 대규모 애플리케이션에서도 원활히 동작하도록 설계되었다. 내부적으로는 단일 store + 구독 모델을 사용하여, 필요한 상태 조각만 선택적으로 구독함으로써 불필요한 렌더링을 최소화한다.
Zustand 는 단순한 훅 기반 중앙 Store 모델을 제공한다. create((set, get) => ({ state, actions })) 형태로 store 를 정의하고, 훅을 통해 직접 구독하는 직관적인 구성을 취한다.
Redux 의 단방향 데이터 흐름 (Flux 원칙) 은 유지하되, action, reducer, middleware 구조를 강요하지 않는다. 이 라이브러리는 “bearbones”라는 이름처럼 필요한 최소한의 기능만 제공하며, 슬라이스 분리, 모듈 구조, 미들웨어 조합 등은 개발자가 자유롭게 선택할 수 있도록 설계되어 있다.

불필요한 보일러플레이트를 제거하고, 미들웨어 사용을 선택적으로 단순화
모든 상태를 하나의 Store 에서 관리하며, 여러 컴포넌트가 이를 필요에 따라 구독
데이터가 단방향으로 흐르는 Flux 패턴과 유사하게 동작하므로, 상태 변경의 경로가 명확함
// store.tsimport { create } from 'zustand';type AppState = {userName: string;theme: 'light' | 'dark';setUserName: (name:
Jotai는 “React를 위한 원시적이고 유연한 상태 관리” 를 목표로 하는 라이브러리다.
Atomic 접근법, Bottom‑up 구조, 그리고 useState와 유사한 단순함을 핵심 철학으로 삼는다.
Primitive – useState 에 최대한 가깝게
Jotai의 atom은 전역적인 useState처럼 동작한다. atom(initialValue)와 useAtom(atom)의 조합은 useState와 1:1로 대응되며, “React 개발의 기본으로 돌아가자”는 철학을 반영한다. 즉, 복잡한 Context나 Redux의 개념 없이 훅만으로 전역 상태를 간결하게 관리할 수 있다.
Flexible – 원자들을 조합해서 복잡한 상태 구성
각 atom은 독립적인 상태 단위지만, 다른 atom을 읽는 derived atom을 통해 서로 연결될 수 있다.
이 방식을 통해 작은 상태 조각들을 조합하여 복잡한 상태 모델을 만들 수 있으며, atom 간의 의존 그래프를 기반으로 자동 렌더링 최적화가 이루어진다.
Atomic / Bottom‑up 접근
Jotai는 Recoil에서 영감을 받은 원자적 상태 관리 방식을 사용한다. 앱 전체를 하나의 거대한 상태 트리로 관리하기보다는, 작은 atom들의 네트워크로 상태를 구성한다.
즉, 상위에서 store를 설계해 내려보내는 Top-down 방식이 아니라, 컴포넌트 단에서 필요한 atom을 하나씩 쌓아 올리는 Bottom‑up 방식으로 상태를 설계한다.

// atoms.tsimport { atom } from 'jotai';// 가장 기본적인 atom (Primitive atom)export const countAtom = atom(0);// Counter.tsximport { useAtom } from
// atoms.tsimport { atom } from 'jotai';export const priceAtom = atom(10000);export const quantityAtom = atom(1);// 다른 atom을 읽어 만들어지는 파생 상태 (Derived atom)
// CartSummary.tsximport { useAtom } from 'jotai';import { priceAtom, quantityAtom, totalAtom } from './atoms';export function CartSummary() {const