Context를 넘어서 (Zustand)

782025년 11월 01일4

useContext는 Prop Drilling 문제를 해결하는 훌륭한 도구였지만, 개발자들은 곧 그것을 ‘전역 상태 관리(Global State Management)’를 위한 만능 해결책으로 사용하려다 새로운 문제에 부딪혔다.

한 개발팀이 useContextuseReducer를 조합하여 Redux와 비슷한 형태의 전역 스토어(Store)를 만들었다.

// 스토어 설정
const StoreContext = createContext();

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(rootReducer, initialState);
  const value = { state, dispatch };
  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

// 스토어 사용
function UserProfile() {
  const { state } = useContext(StoreContext);
  return <p>Username: {state.user.name}</p>;
}

function CartCounter() {
  const { state } = useContext(StoreContext);
  return <p>Cart items: {state.cart.items.length}</p>;
}

이 패턴은 잘 작동하는 것처럼 보였다.
하지만 치명적인 성능 문제가 숨어 있었다.
useContext는 Context의 value가 변경될 때마다, 해당 Context를 구독하는 모든 컴포넌트를 강제로 리렌더링시켰다.

위 예제에서, 사용자가 장바구니에 상품을 추가하여 state.cart가 바뀌었다고 가정해보자.
StoreProvider는 새로운 state 객체로 새로운 value를 생성하고, 리렌더링된다.
그 결과, useContext(StoreContext)를 사용하는 모든 컴포넌트, 즉 <UserProfile><CartCounter>가 모두 리렌더링된다.

<UserProfile>state.cart 데이터와 아무런 관련이 없음에도 불구하고, state 객체 자체가 새로워졌다는 이유만으로 불필요한 렌더링을 겪게 되는 것이다. 애플리케이션이 커지고 스토어의 상태가 복잡해질수록, 이 문제는 걷잡을 수 없는 성능 저하를 유발했다.

이 문제를 해결하기 위해 Redux와 같은 라이브러리는 useSelector 훅을 통해, 상태의 특정 부분만 구독하고 그 부분이 변경될 때만 리렌더링하는 정교한 최적화 로직을 제공했다.

하지만 많은 개발자들은 “간단한 전역 상태 관리를 위해 Redux의 모든 보일러플레이트를 감수하고 싶지 않다”고 느꼈다.
그들은 useContext의 편리함과 Redux의 최적화 성능, 이 두 가지 장점만을 취한 더 가벼운 해결책을 원했다.

이러한 요구에 부응하여, Zustand라는 이름의 새로운 상태 관리 라이브러리가 등장했다.
Zustand는 독일어로 ‘상태’를 의미했다. 이름처럼, 이 라이브러리는 상태 관리의 본질에만 집중했다.

Zustand의 접근 방식은 혁신적이었다.
그것은 Context API를 전혀 사용하지 않았다.

import create from 'zustand';

// 스토어를 생성한다. 훅처럼 생겼다.
const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

function BearCounter() {
  // 스토어에서 필요한 상태만 선택하여 가져온다.
  const bears = useStore(state => state.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  // 스토어에서 필요한 액션 함수만 가져온다.
  const increasePopulation = useStore(state => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

마법 같은 일이 일어났다.
애플리케이션을 <Provider>로 감쌀 필요가 전혀 없었다. create 함수로 스토어를 만들고, 어떤 컴포넌트에서든 useStore 훅을 호출하기만 하면 전역 상태에 접근하고 업데이트할 수 있었다.

Zustand는 내부적으로 Redux의 useSelector와 유사한 구독 메커니즘을 사용하여, 컴포넌트가 구독하는 상태 조각이 실제로 변경되었을 때만 리렌더링을 유발했다. 덕분에 useContext가 가진 성능 문제를 완벽하게 해결했다.

Zustand는 훅의 유연성이 얼마나 강력한지를 보여주는 결정적인 사례였다.
훅은 개발자들이 리액트의 내장 기능(Context)에 얽매이지 않고, 상태 관리라는 근본적인 문제를 해결하는 자신들만의 독창적이고 효율적인 패턴을 창조할 수 있게 해주었다.

Redux의 무거움과 Context의 성능 문제 사이에서 고민하던 개발자들에게, Zustand는 간결하고 강력한 제3의 길을 제시했다. 훅 생태계는 이제, 다양한 문제와 요구사항에 맞춰 선택할 수 있는 풍부한 솔루션들로 가득 차고 있었다.