"이 상태는 어디에 두어야 하지?" React 개발에서 가장 자주 마주치는 이 질문에 대한 체계적인 답은 무엇일까요?

개념 정의

전역 상태 설계란 애플리케이션의 각 상태를 가장 적절한 위치에 배치 하는 전략입니다. 모든 상태를 전역에 두는 것도, 모든 상태를 로컬에 두는 것도 최적이 아닙니다. 상태의 성격과 사용 범위에 맞는 위치 를 찾는 것이 핵심입니다.

왜 필요한가

상태를 잘못된 곳에 두면 다음과 같은 문제가 발생합니다.

  • **과도한 전역 상태 **: 모든 것이 전역 → 불필요한 리렌더링, 복잡한 디버깅
  • ** 과도한 로컬 상태 **: props drilling이 깊어짐, 중복 로직
  • ** 혼합 관리 **: 서버 데이터를 클라이언트 상태로 복사 → 동기화 문제

State Colocation — 상태는 가까이

Kent C. Dodds가 제안한 이 원칙은 "** 상태를 사용하는 곳에 최대한 가까이 두라 **"는 것입니다.

JSX
// ❌ 전역에 둘 필요가 없는 상태
const useGlobalStore = create((set) => ({
  isModalOpen: false,        // 특정 페이지에서만 사용
  searchInput: '',           // 검색 바에서만 사용
  tooltipVisible: false,     // 특정 컴포넌트에서만 사용
}));

// ✅ 사용하는 곳에 가까이
function SearchBar() {
  const [input, setInput] = useState(''); // 여기서만 사용
  return <input value={input} onChange={e => setInput(e.target.value)} />;
}

function ProductModal() {
  const [isOpen, setIsOpen] = useState(false); // 여기서만 사용
  return (/* ... */);
}

상태 위치 결정 흐름

PLAINTEXT
이 상태를 사용하는 컴포넌트가 하나인가?
├── 예 → 해당 컴포넌트에 useState
└── 아니오
    ├── 형제 또는 가까운 조상-자손 관계인가?
    │   ├── 예 → Lifting State Up (상태 끌어올리기)
    │   └── 아니오
    │       ├── 서버에서 온 데이터인가?
    │       │   ├── 예 → TanStack Query / SWR
    │       │   └── 아니오
    │       │       ├── URL에 반영되어야 하나?
    │       │       │   ├── 예 → useSearchParams / URL 상태
    │       │       │   └── 아니오 → 전역 상태 (Zustand / Redux / Context)
    │       │       └──
    │       └──
    └──

Lifting State Up vs Pushing State Down

Lifting State Up (끌어올리기)

두 자식 컴포넌트가 같은 상태를 공유해야 할 때, 공통 부모로 상태를 올립니다.

JSX
function ProductPage() {
  // 필터와 목록이 같은 상태를 공유
  const [filter, setFilter] = useState('all');

  return (
    <div>
      <FilterBar filter={filter} onFilterChange={setFilter} />
      <ProductList filter={filter} />
    </div>
  );
}

Pushing State Down (내려보내기)

불필요하게 높은 곳에 있는 상태를 실제 사용처로 내립니다.

JSX
// ❌ App에서 관리할 필요 없는 상태
function App() {
  const [color, setColor] = useState('red');
  return (
    <div>
      <Header />
      <ColorPicker color={color} onChange={setColor} />
      <Content />
    </div>
  );
}

// ✅ 상태를 사용하는 컴포넌트로 내리기
function App() {
  return (
    <div>
      <Header />
      <ColorPicker /> {/* 내부에서 state 관리 */}
      <Content />
    </div>
  );
}

전역 상태가 필요한 시점

다음 조건 중 하나라도 해당하면 전역 상태를 고려합니다.

  1. **트리 전체에서 접근 **: 인증 정보, 테마, 언어 설정
  2. ** 멀리 떨어진 컴포넌트 간 공유 **: 장바구니(헤더 배지 + 장바구니 페이지)
  3. ** 여러 페이지에 걸친 상태 **: 위저드 폼의 단계 데이터
JSX
// 전역이 적합한 예시들
const useAuthStore = create((set) => ({
  user: null,          // 거의 모든 곳에서 참조
  isAuthenticated: false,
}));

const useCartStore = create((set) => ({
  items: [],           // 헤더, 장바구니 페이지, 결제 페이지에서 사용
  total: 0,
}));

여러 솔루션 조합

실제 프로젝트에서는 ** 여러 도구를 조합 **하는 것이 가장 현실적입니다.

JSX
function App() {
  return (
    // 서버 상태: TanStack Query
    <QueryClientProvider client={queryClient}>
      {/* 라우팅 + URL 상태 */}
      <BrowserRouter>
        <Routes>
          <Route path="/products" element={<ProductPage />} />
        </Routes>
      </BrowserRouter>
    </QueryClientProvider>
  );
}

// Zustand: 클라이언트 전역 상태
const useUIStore = create((set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })),
}));

function ProductPage() {
  // URL 상태: 검색, 필터, 페이지
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') || 'all';

  // 서버 상태: 상품 데이터
  const { data: products } = useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
  });

  // 로컬 상태: 이 페이지에서만 사용
  const [selectedId, setSelectedId] = useState(null);

  // 전역 클라이언트 상태
  const sidebarOpen = useUIStore(s => s.sidebarOpen);

  return (/* ... */);
}

각 솔루션별 적합한 사용처

도구적합한 상태특성
useState단일 컴포넌트 로컬 상태가장 간단
useReducer복잡한 로컬 상태로직 집중
Context테마, 언어 등 변경 빈도 낮은 전역내장, Provider 필요
ZustandUI 전역 상태간결, 선택적 구독
Redux Toolkit대규모 앱의 복잡한 상태DevTools, 미들웨어
TanStack Query서버 상태캐싱, 재검증
URL (searchParams)공유 가능한 상태북마크, 뒤로가기

안티패턴

1. 모든 것을 전역에

JSX
// ❌ 모달 열림 상태까지 전역으로?
const useStore = create((set) => ({
  isDeleteModalOpen: false,
  isEditModalOpen: false,
  deleteTargetId: null,
  editFormData: {},
  // ... 수십 개의 UI 상태
}));

2. 서버 데이터를 전역 스토어에 복사

JSX
// ❌ Redux에 서버 데이터 저장
dispatch(setUsers(fetchedUsers));
// 동기화 문제, 캐시 관리를 직접 해야 함

// ✅ TanStack Query가 캐시를 관리
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });

3. Prop Drilling을 피하려고 무조건 전역

JSX
// ❌ props 3-4단계 전달이 싫어서 전역?
// → 먼저 컴포넌트 합성(children)을 시도

// ✅ 합성으로 해결
function Page() {
  const [user, setUser] = useState(null);
  return (
    <Layout>
      <Sidebar>
        <UserProfile user={user} /> {/* 직접 전달 */}
      </Sidebar>
    </Layout>
  );
}

의사결정 체크리스트

새로운 상태를 추가할 때 순서대로 질문합니다.

  1. 이 상태는 하나의 컴포넌트 에서만 사용하나? → useState
  2. 가까운 몇 개의 컴포넌트 가 공유하나? → Lifting State Up
  3. 합성(children)으로 drilling을 **제거할 수 있나 **? → 컴포넌트 합성
  4. ** 서버에서 온 데이터 **인가? → TanStack Query / SWR
  5. URL에 반영 되어야 하나? → searchParams
  6. 변경 빈도가 낮은 전역 설정인가? → Context
  7. ** 변경 빈도가 높은** 전역 상태인가? → Zustand / Redux

주의할 점

모든 상태를 전역 스토어에 넣는 실수

전역 스토어에 로컬 UI 상태(모달 열림/닫힘, 입력 필드 값 등)까지 넣으면, 관련 없는 컴포넌트의 리렌더링이 증가하고 디버깅이 어려워집니다. State Colocation 원칙에 따라 상태를 사용하는 곳에 최대한 가까이 두어야 합니다.

서버 데이터를 클라이언트 state에 복사

useEffect로 서버 데이터를 받아 useState에 저장하면, 캐싱, 재검증, 백그라운드 동기화를 모두 직접 구현해야 합니다. 서버 상태는 TanStack Query, SWR 같은 전용 도구에 맡기는 것이 올바릅니다.

정리

항목설명
State Colocation상태를 사용하는 곳에 최대한 가까이 배치
전역 상태정말 전역이 필요한 경우(인증, 테마)에만 사용
서버 상태TanStack Query / SWR — 캐싱과 재검증 자동화
URL 상태searchParams — 공유, 북마크가 필요한 데이터
핵심 질문"전역에 둘까?"가 아니라 "이 상태의 올바른 위치는 어디인가?"

상태 관리에서 가장 중요한 것은 특정 라이브러리의 선택이 아니라, "이 상태의 올바른 위치는 어디인가?"라는 질문에 답할 수 있는 판단력입니다.

댓글 로딩 중...