Develop

Zustand 상태관리 완벽 가이드 — React 전역 상태를 관리하는 방법

과도한 리렌더링 유발과 복잡한 환경 설정 문제를 가진 Context API와 Redux의 대안으로 떠오른 경량 상태관리 도구인 Zustand의 타입 안전한 스토어 구성법과 로컬스토리지 동기화 및 SSR mismatch 해결책을 수록했습니다.

Zustand상태관리ReactTypeScriptfrontend
Zustand 스토어에서 React 컴포넌트가 전역 상태를 구독하는 구조를 나타낸 다이어그램 — 상태관리 예시
  • ·Zustand는 Redux보다 설정이 훨씬 간단하고 보일러플레이트가 적은 경량 상태관리 라이브러리다
  • ·Zustand 스토어는 Provider 없이 훅으로 바로 쓸 수 있어서 컴포넌트 트리 구조에 영향을 주지 않는다
  • ·Zustand의 persist 미들웨어로 상태를 로컬 스토리지나 세션 스토리지에 자동으로 저장하고 불러올 수 있다
  • ·구독할 상태를 선택자(selector)로 좁게 지정하면 무관한 상태 변경 시 리렌더링이 발생하지 않는다
리액트 기본 `Context API` 를 차용해 다크모드나 장바구니 데이터를 관리하다가, 전역 상태가 조금만 갱신되어도 해당 Context 하위에 묶인 수십 개의 컴포넌트가 싹 다 불필요하게 리렌더링을 일으켜 프레임이 깨지는 아쉬움을 목격했습니다. `Zustand` 로 상태 전송 라인을 전면 갈아탄 후에는, 복잡하게 Provider 노드로 트리를 휘감을 필요 없이 훅 선언 한 줄로 전역 스토어 값을 당겨올 수 있게 되었고, 내가 구독한 특정 상태 슬라이스만 정확하게 리렌더링되어 성능과 가독성 두 마리 토끼를 단숨에 쟁취했습니다.

1. 경량 스토어 구조와 타입 셋업

Provider 중첩을 타파하는 Zustand 기반 React 전역 상태 스토어 설계

과거 Redux를 세팅할 때는 액션 타입, 디스패처, 리듀서, 스토어 인스턴스를 파일마다 나누어 설계해야 해서 파일 부피가 너무 비대했고, Context API는 상태 하나가 갱신될 때마다 전체 하위 트리를 강제 강수 렌더링하는 병목이 고질적이었습니다. Zustand 는 단 한 줄의 create 함수 구문으로 전역 메모리 저장소를 개설합니다. 액션 변경용 함수(set)를 상태 변수와 한 몸의 단일 객체로 구성하여 묶어두죠. 특히 리액트 트리 최상단에 Context Provider 껍데기를 주입하지 않으므로 컴포넌트 파일들의 계층 독립성을 완벽히 보장해 줍니다.

2. 영구 보존 및 SSR 플래시 에러 방어

새로고침에도 무너지지 않는 Zustand persist 로컬스토리지 보존법

사용자의 로그인 잔여 세션 상태나 다크 모드 활성화 유무, 혹은 미결제 장바구니 리스트 같은 정보들은 브라우저 새로고침 시에도 소멸되지 않게 보존해야 합니다. Zustand가 지원하는 persist 미들웨어 래퍼로 스토어 선언부를 감싸주고 저장소 식별 고유 name 키를 매핑해 주면, 젠킨스 수준이 아닌 Zustand 코어가 상태가 갱신될 때마다 브라우저 localStorage 에 JSON 문자열로 데이터 동기화를 자동 수행해 줍니다.

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface ThemeState {
  isDarkMode: boolean;
  toggleTheme: () => void;
}

// persist 미들웨어를 엮어 상태 변경 사항을 로컬 스토리지에 자동 보존
export const useThemeStore = create<ThemeState>()(
  persist(
    (set) => ({
      isDarkMode: false,
      toggleTheme: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
    }),
    {
      name: 'theme-storage-key', // localStorage 에 보존될 고유 식별 명칭
    }
  )
);

Next.js SSR 모드 하에서 Zustand persist Mismatch 해결 팁

하지만 이 persist 기능을 Next.js 서버 사이드 렌더링(SSR) 환경 아래에서 아무 안전가드 없이 도입하면 악명 높은 **Hydration Mismatch 빨간 에러 메시지를 얻어맞게 됩니다.** 서버가 사전 마크업을 그릴 때는 로컬스토리지에 있는 유저의 다크모드 값을 읽을 수 없어 라이트 모드로 코드를 뱉고, 브라우저가 화면을 넘겨받은 직후 로컬스토리지 캐시를 읽어 다크 클래스를 뒤늦게 주입하면서 서버/클라이언트 마크업 내용 불일치 충돌을 일으키기 때문입니다. 이 현상을 방어하려면 컴포넌트 마운트 상태 여부를 추적하는 리액트 훅 코드를 가미해 브라우저 로딩이 확보된 안전한 시점에 스토어 값을 읽어가게 예외 구조를 심어 두어야 합니다.

import { useEffect, useState } from 'react';
import { useThemeStore } from '@/stores/themeStore';

// SSR Hydration Mismatch 플래시 에러를 예방하는 안전 스토어 바인딩 훅
export function useThemeSafeStore() {
  const [mounted, setMounted] = useState(false);
  const store = useThemeStore();
  
  useEffect(() => {
    setMounted(true);
  }, []);

  return mounted ? store : { isDarkMode: false, toggleTheme: () => {} };
}

3. 구조적 분리와 생태계 조율

슬라이스 패턴과 devtools를 활용한 Zustand 상태관리 구조화 요령

프로젝트 덩치가 커져 다양한 UI 전역 변수들이 난입하면 단일 파일 스토어는 스파게티 코드로 굳어집니다. 이럴 때는 상태와 액션을 성격별로 쪼갠 뒤 단일 스토어로 수렴 합체하는 슬라이스(Slice) 패턴을 취하는 것이 아키텍처 관점에서 이롭습니다. 추가로 devtools 미들웨어를 스토어 상단에 링킹해 두면, 크롬 브라우저의 Redux DevTools 확장 프로그램을 켜고 상태가 어떤 액션 함수에 의해 타격되어 변조되어 흘러가는지 타임라인 단위로 가시적 역추적이 가능해져 디버깅 스피드가 압도적으로 날아오릅니다.

Context API 및 Redux 대비 Zustand 상태관리 라이브러리 선정 기준

전역 상태 관리 라이브러리의 춘추전국시대에서 어떤 툴을 선택해야 하는지는 팀의 유지 보수 비용과 프로젝트 규모에 수반됩니다. 단순 테마 스위칭 수준의 마이너 정보라면 패키지 설치 없는 기본 Context API가 맞습니다. 그러나 기능 조직이 거대하고 상태 변조 추적 로그를 엄격히 한 곳에 모아야 하는 금융/정산 대형 웹앱이라면 Redux Toolkit이 제값을 합니다. 그 중간 지대에 위치해 러닝 커브를 줄이고, 보일러플레이트 오버헤드 없이 신속하고 성능 안전한 전역 상태 관리를 꿈꾼다면 Zustand 가 현대 웹 개발의 최선의 카드로 등극합니다.

자주 묻는 질문

Zustand useStore() 훅을 불러다 쓸 때 왜 통째로 안 가져오고 화살표 콜백 선택자(Selector)를 기재해 가져오나요?+

만약 `const store = useCounterStore()` 처럼 스토어 전체를 긁어오면, 컴포넌트 본문에서 쓰지 않는 다른 무관한 상태(예: title 등)가 변경되어도 내 컴포넌트까지 불필요하게 리렌더링 연산을 겪기 때문입니다. `const count = useCounterStore(state => state.count)` 처럼 필요한 조각만 선택자 매핑을 해두어야 해당 값이 바뀔 때만 리렌더링이 발생하는 성능 이점을 온전히 누릴 수 있습니다.

Zustand 상태 값을 React 컴포넌트 밖의 일반 순수 자바스크립트/타입스크립트 유틸 파일에서도 읽을 수 있나요?+

네, 가능합니다. Zustand는 훅 스펙 외에 일반 인스턴스 메서드도 개방해 줍니다. 일반 JS 파일 내부에서 `useCounterStore.getState().count` 를 호출하면 컴포넌트가 아닌 일반 함수 코드 안에서도 즉시 메모리 상태를 취득할 수 있고, `useCounterStore.setState(...)` 로 상태 변조 명령을 쏠 수도 있습니다.

관련 글