Taeyoung Kim

Engineering

Zustand 핵심 개념

Zustand 핵심 개념 학습 내용을 정리한 백필 노트입니다.

이 글은 2025년 학습 기록을 블로그 형식으로 정리한 백필 노트입니다.


이 문서는 React의 상태 관리 라이브러리인 Zustand(v5.0.5 기준)의 핵심 개념과 사용법, 주요 미들웨어 활용법을 정리합니다.

1. Zustand 개요 및 필요성

  • Zustand란? 작고 빠르며 확장 가능한 React용 상태 관리(Store) 라이브러리입니다.
  • 스토어(Store)의 역할: 애플리케이션의 여러 상태(데이터)를 중앙에서 관리하는 공간입니다.
  • 필요성 (Prop Drilling 문제 해결):
    • React의 기본 데이터 전달 방식(Props)은 컴포넌트 구조가 깊어질수록 중간 컴포넌트들이 불필요하게 데이터를 전달해야 하는 'Prop Drilling' 문제를 야기합니다.
    • 스토어를 사용하면 어떤 컴포넌트든 중앙 저장소에 직접 접근할 수 있어, 컴포넌트 간 결합도를 낮추고 코드의 유지보수성을 크게 향상시킵니다.

2. 기본 사용법

  1. **설치:**Bash

    npm i zustand
    
  2. **스토어 생성:**TypeScript

    • create 함수를 사용하여 스토어를 생성합니다. 콜백 함수는 set(상태 변경)과 get(상태 조회) 함수를 인자로 받습니다.
    • 콜백이 반환하는 객체는 **상태(State)**와 **액션(Action)**으로 구성됩니다.
    • 생성된 스토어는 use 접두사를 붙인 커스텀 훅(예: useCountStore)으로 사용됩니다.
    import { create } from 'zustand';
    
    export const useCountStore = create<MyState & MyActions>(set => ({
      count: 1, // 상태(State)
      increase: () => set(state => ({ count: state.count + 1 })), // 액션(Action)
    }));
    
  3. **컴포넌트에서 사용:**TypeScript

    • 생성한 스토어 훅을 컴포넌트 내에서 호출하여 상태와 액션을 사용합니다.
    • 선택자(Selector) 함수 (state => state.count)를 사용하여 필요한 상태나 액션만 가져옵니다. 이는 불필요한 리렌더링을 방지하는 핵심적인 최적화 방법입니다.
    • 주의: 선택자 없이 훅 전체(useCountStore())를 호출하면, 스토어의 어떤 상태가 변경되어도 해당 훅을 사용하는 모든 컴포넌트가 리렌더링되므로 권장되지 않습니다.
    import { useCountStore } from './store/count';
    
    export default function App() {
      const count = useCountStore(state => state.count);
      const increase = useCountStore(state => state.increase);
      // ...
    }
    

3. 다중 상태 선택 (useShallow)

  • 여러 상태나 액션을 한 번에 가져오면서도 불필요한 리렌더링을 방지하고 싶을 때 useShallow 훅을 사용합니다.

  • 선택자 함수에서 반환하는 객체나 배열의 얕은(shallow) 비교를 통해 상태 변경을 감지합니다.TypeScript

    import { useShallow } from 'zustand/shallow';
    
    const { count, increase } = useCountStore(
      useShallow(state => ({ count: state.count, increase: state.increase }))
    );
    

4. 스토어 관리 패턴

  • 액션 분리: 스토어 내에 actions 객체를 만들어 모든 액션을 그룹화하면 관리가 용이합니다.
  • 상태 초기화: initialState 객체를 별도로 정의하고, resetState 액션을 만들어 set(initialState)를 호출함으로써 상태를 초기값으로 되돌릴 수 있습니다.
  • 상태 삭제: set(state => omit(state, keys), true)와 같이 set의 두 번째 인자를 true로 설정하고 lodash-esomit 같은 유틸리티를 사용하면 상태를 병합하는 대신 덮어써서 특정 상태를 삭제할 수 있습니다.

5. 미들웨어 (Middleware)

Zustand는 미들웨어를 통해 스토어의 기능을 확장할 수 있습니다. 여러 미들웨어를 중첩하여 사용할 수 있습니다. create(middlewareA(middlewareB(...)))

  • combine (상태 타입 추론):
    • TypeScript에서 상태 타입을 직접 정의하지 않고, 초기 상태 객체로부터 타입을 추론하게 해줍니다.
  • immer (중첩 객체 변경):
    • immer 라이브러리(npm i immer) 설치가 필요합니다.
    • 중첩된 객체 상태를 변경할 때 불변성을 신경 쓰지 않고 state.user.name = 'newName'과 같이 직접 수정하는 것처럼 코드를 작성할 수 있어 매우 편리합니다.
  • subscribeWithSelector (상태 구독):
    • 스토어의 특정 상태 변경을 감지하여 콜백 함수(리스너)를 실행합니다.
    • 이를 통해 한 상태의 변경에 따라 다른 상태를 업데이트하는 계산된 상태(Computed State)와 유사한 로직을 구현할 수 있습니다.
    • 컴포넌트에서는 useEffect 훅을 사용하여 구독하고, 언마운트 시 구독을 해제해야 합니다.
  • persist (스토리지 사용):
    • 스토어의 상태를 웹 스토리지(기본값: localStorage)에 자동으로 저장하고 불러옵니다.
    • 페이지를 새로고침해도 상태가 유지됩니다.
    • name 옵션으로 스토리지에 저장될 고유한 키를 반드시 지정해야 합니다.
    • 주의: 함수와 같은 직렬화할 수 없는 데이터(액션)는 저장되지 않습니다.
  • devtools (개발자 도구):
    • Redux DevTools 브라우저 확장 프로그램과 연동하여 상태 변화를 시각적으로 추적하고 디버깅할 수 있게 해줍니다.

미들웨어 조합 예시:

TypeScript

import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector, combine } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

const initialState = { count: 1, double: 2 };

export const useCountStore = create(
  devtools( // 1. 개발자 도구
    persist( // 2. 스토리지 저장
      subscribeWithSelector( // 3. 상태 구독
        immer( // 4. 중첩 객체 변경
          combine(initialState, (set, get) => ({ // 5. 타입 추론 및 액션 정의
            increase: () => set(state => { state.count += 1; }),
          }))
        )
      ),
      { name: 'countStore' } // persist 옵션
    )
  )
);