본문 바로가기
개발

[State] Props Drilling의 고통: Zustand로 10분 만에 끝내는 전역 상태 관리 도입기

by 돌미나리는야생미나리 2026. 8. 21.

리액트(React)로 컴포넌트를 분리하며 프로젝트를 개발하다 보면 누구나 한 번쯤 마주치는 악몽이 있습니다. 바로 '프롭스 드릴링(Props Drilling)'입니다.

로그인한 유저 정보나 다크모드 설정값처럼 앱 전체에서 쓰이는 데이터를 단 하나의 하위 컴포넌트에 전달하기 위해, 부모 ➔ 자식 ➔ 손자 ➔ 증손자 컴포넌트를 거쳐 불필요하게 props를 계속 넘겨주다 보면 코드가 지저분해지고 유지보수가 불가능해집니다.

처음에는 Redux(리덕스)를 도입해 보려 했지만, 단순한 상태 하나 만드는데 Action, Reducer, Store 등 보일러플레이트 코드가 너무 많아 배보다 배꼽이 더 큰 느낌을 받았습니다.

오늘은 보일러플레이트 없이 가볍고 직관적인 Zustand(주스탠드)를 도입하여 프롭스 드릴링을 단 10분 만에 해결했던 경험과 실무 활용 꿀팁을 공유해 보려고 합니다.

1. 내가 마주친 문제: "증손자 컴포넌트 하나 때문에 부모들이 고통받다"

헤더에 있는 장바구니 카운트와, 메인 상품 목록 페이지에 있는 '장바구니 담기' 버튼을 동기화해야 하는 상황이었습니다.

TypeScript
 
// ❌ 프롭스 드릴링으로 고통받던 과거 컴포넌트 구조
<App>
  <Header cartCount={cartCount} /> {/* 헤더에 전달 */}
  <MainPage>
    <ProductList>
      <ProductCard onAddToCart={handleAddToCart} /> {/* 3단계를 거쳐 함수 전달 */}
    </ProductList>
  </MainPage>
</App>

중간에 있는 MainPage나 ProductList는 onAddToCart 함수를 직접 쓰지도 않으면서 단순히 자식에게 넘겨주기 위해 props를 억지로 받아야 했습니다. 이 구조는 나중에 컴포넌트 위치가 바뀌거나 기능이 추가될 때마다 치명적인 리팩토링 비용을 발생시켰습니다.

2. 왜 Redux 대신 Zustand를 선택했는가?

전역 상태 라이브러리를 고민할 때 가장 먼저 떠오른 것은 Redux였지만, 작은 규모의 기능 하나를 추가하기 위해 작성해야 할 보일러플레이트 코드가 너무 많았습니다. 반면 Zustand는 다음과 같은 압도적인 장점이 있었습니다.

  1. 단순한 훅(Hook) 기반: 별도의 Context Provider로 앱 전체를 감쌀 필요가 없음.
  2. 극도로 가벼운 번들 크기: 수십 KB에 달하는 다른 라이브러리에 비해 약 1KB 수준으로 초경량.
  3. 타입스크립트(TypeScript) 완벽 지원: 복잡한 제네릭 설정 없이 직관적인 타입 추론 가능.

3. 실무 적용: 10줄로 만드는 Zustand 전역 스토어

Zustand의 핵심은 create 함수를 사용해 스토어(Store)를 정의하고, 필요한 컴포넌트에서 일반 커스텀 훅처럼 가져다 쓰는 것입니다.

TypeScript
 
// store/useCartStore.ts (장바구니 전역 스토어)
import { create } from 'zustand';

interface CartState {
  cartCount: number;
  addToCart: () => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>((set) => ({
  cartCount: 0,
  // set 함수를 사용해 상태를 불변성 유지하며 업데이트
  addToCart: () => set((state) => ({ cartCount: state.cartCount + 1 })),
  clearCart: () => set({ cartCount: 0 }),
}));

Provider로 최상단 App 컴포넌트를 감싸는 과정조차 필요 없이, 이 파일 하나로 전역 상태 세팅이 끝납니다.

인터페이스 정의와 Zustand create 스토어 구조


4. 컴포넌트에서 사용하기: "필요한 곳에서 바로 꺼내 쓴다"

이제 프롭스 드릴링 없이, 상태가 필

요한 컴포넌트에서 훅을 호출하기만 하면 됩니다.

TypeScript
 
// 1. 헤더 컴포넌트: cartCount 값만 구독
import { useCartStore } from '../store/useCartStore';

export function Header() {
  const cartCount = useCartStore((state) => state.cartCount);
  return <header>장바구니: 🛒 {cartCount}개</header>;
}

// 2. 상품 카드 컴포넌트: addToCart 함수만 호출
export function ProductCard() {
  const addToCart = useCartStore((state) => state.addToCart);
  return <button onClick={addToCart}>장바구니 담기</button>;
}

중간 컴포넌트들을 거칠 필요 없이 각자 필요한 데이터와 함수만 직접 구독하므로, 불필요한 리렌더링도 방지되고 코드가 엄청나게 깔끔해졌습니다.

담은 상품 수에 따라 달라지는 금액

5. 결론 및 느낀 점

Zustand를 도입하고 나서 리액트 상태 관리의 피로도가 절반 이하로 줄어들었습니다.

  • 상태 하나 만드는데 1분도 채 걸리지 않는 압도적인 생산성
  • 불필요한 props 전달이 사라져 깔끔해진 컴포넌트 트리 구조

컴포넌트 깊이가 깊어지면서 props를 넘겨주는 데 스트레스를 받고 계신다면, 무거운 라이브러리 대신 가볍고 직관적인 Zustand로 전역 상태 관리를 시작해 보시길 강력히 추천합니다!