Zustand와 Redux 구조 비교: 외부 스토어와 구독(Subscription) 메커니즘 이해하기
Redux와 Zustand의 아키텍처 차이를 분석하고, Zustand가 React 트리를 벗어나 어떻게 컴포넌트를 구독하고 리렌더링을 제어하는지 정리합니다.
2025년 3월 13일
최근 프론트엔드 프로젝트를 진행하면서 전역 상태 관리 도구로 Redux 대신 Zustand를 도입하는 사례가 늘고 있다. 흔히 "보일러플레이트 코드가 적고 사용하기 편하다"는 이유를 꼽지만, 내부 동작 원리를 들여다보면 두 라이브러리는 상태를 쥐고 있는 위치와 컴포넌트를 구독(Subscribe)하는 아키텍처 자체가 다르다.
단순한 문법 차이를 넘어, Zustand가 어떻게 React의 렌더링 사이클을 최적화하는지 아키텍처 관점에서 정리해 본다.
1. 상태가 존재하는 위치: Context API vs 외부 스토어(External Store)
두 라이브러리의 가장 큰 구조적 차이는 상태(State)가 React 트리 내부에 종속되는지 여부다.
- Redux: 기본적으로 단방향 데이터 흐름(Flux) 아키텍처를 따른다. 중요한 점은 React 컴포넌트에서 Redux 스토어에 접근하려면 최상단에
<Provider store={store}>를 씌워야 한다는 것이다. 이는 React의 Context API를 기반으로 상태를 주입하기 때문이다. - Zustand: 스토어를 생성할 때 Provider가 필요 없다. Zustand의 상태는 React 컴포넌트 트리 내부가 아니라, React 바깥의 메모리(클로저, Closure)에 독립적으로 존재한다. 이를 외부 스토어(External Store) 패턴이라고 한다.
이러한 구조적 차이는 Next.js의 App Router 환경에서 큰 이점으로 작용한다. Zustand는 Provider가 없으므로 최상단 레이아웃을 클라이언트 컴포넌트(use client)로 만들 필요 없이, 서버 컴포넌트(RSC) 아키텍처를 깔끔하게 유지할 수 있다.
2. Zustand의 구독 메커니즘: 발행-구독(Pub/Sub) 패턴
Zustand가 Provider(Context API) 없이도 상태 변경을 컴포넌트에 알릴 수 있는 이유는 내부적으로 발행-구독(Pub/Sub) 패턴을 구현했기 때문이다.
- 스토어 생성:
create함수를 호출하면 클로저 공간에state객체와 이 상태를 구독하는 컴포넌트들을 모아둘listeners(Set 혹은 배열)가 생성된다. - 구독 (Subscribe): 컴포넌트에서
useStore훅을 호출하면, 해당 컴포넌트를 강제로 리렌더링 시킬 수 있는 함수(React의forceUpdate역할)가listeners에 등록된다. - 상태 변경 (Publish):
set함수를 통해 스토어의 상태가 업데이트되면, Zustand는listeners를 순회하며 등록된 모든 렌더링 함수를 실행하여 컴포넌트 화면을 갱신한다.
즉, React의 상태 관리 생명주기에 기대지 않고 자바스크립트의 기본 동작 원리를 활용해 컴포넌트 리렌더링을 직접 트리거하는 방식이다.
3. 리렌더링 최적화 원리: 선택적 구독 (Selectors)
Context API 기반의 관리 방식은 Provider 하위의 값이 하나라도 바뀌면 해당 Context를 구독하는 모든 컴포넌트가 불필요하게 렌더링될 위험이 있다. Zustand는 선택자(Selector)를 통해 이 문제를 해결한다.
import { create } from 'zustand';
// 스토어 정의
const useUserStore = create((set) => ({
name: '홍길동',
age: 30,
updateName: (newName) => set({ name: newName }),
updateAge: (newAge) => set({ age: newAge }),
}));
// 컴포넌트 A: 'name' 상태만 구독
const UserName = () => {
const name = useUserStore((state) => state.name);
return <div>{name}</div>;
};
// 컴포넌트 B: 'age' 상태만 구독
const UserAge = () => {
const age = useUserStore((state) => state.age);
return <div>{age}</div>;
};
위 코드에서 updateAge가 실행되어 age 값만 변경되었다면, UserName 컴포넌트는 리렌더링되지 않는다.
Zustand는 내부적으로 이전 상태 값과 Selector가 반환한 새로운 값을 엄격한 동등성 비교(===)로 검사한다. 구독하고 있는 특정 값(Slice)에 변화가 발생했을 때만 해당 컴포넌트의 렌더링 함수를 실행하므로, 데이터 그리드나 복잡한 폼 환경에서 별도의 최적화 작업 없이도 렌더링 성능을 확보할 수 있다.
마무리
Redux는 복잡한 비즈니스 로직과 상태 변화를 중앙에서 엄격하게 추적해야 하는 대규모 애플리케이션에서 여전히 유효한 도구다.
하지만 Zustand는 React 트리에 종속되지 않는 독립적인 스토어 아키텍처, Provider 제거를 통한 서버 컴포넌트 호환성, 그리고 Selector를 통한 세밀한 리렌더링 제어라는 기술적 이점을 제공한다. 단순함 이면에 깔려 있는 이러한 아키텍처의 차이가 상태 관리의 트렌드를 변화시키는 핵심적인 이유다.