Engineering
Zustand 핵심 개념
Zustand 핵심 개념 학습 내용을 정리한 백필 노트입니다.
이 글은 2025년 학습 기록을 블로그 형식으로 정리한 백필 노트입니다.
이 문서는 React의 상태 관리 라이브러리인 Zustand(v5.0.5 기준)의 핵심 개념과 사용법, 주요 미들웨어 활용법을 정리합니다.
1. Zustand 개요 및 필요성
- Zustand란? 작고 빠르며 확장 가능한 React용 상태 관리(Store) 라이브러리입니다.
- 스토어(Store)의 역할: 애플리케이션의 여러 상태(데이터)를 중앙에서 관리하는 공간입니다.
- 필요성 (Prop Drilling 문제 해결):
- React의 기본 데이터 전달 방식(Props)은 컴포넌트 구조가 깊어질수록 중간 컴포넌트들이 불필요하게 데이터를 전달해야 하는 'Prop Drilling' 문제를 야기합니다.
- 스토어를 사용하면 어떤 컴포넌트든 중앙 저장소에 직접 접근할 수 있어, 컴포넌트 간 결합도를 낮추고 코드의 유지보수성을 크게 향상시킵니다.
2. 기본 사용법
-
**설치:**Bash
npm i zustand -
**스토어 생성:**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) })); -
**컴포넌트에서 사용:**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-es의omit같은 유틸리티를 사용하면 상태를 병합하는 대신 덮어써서 특정 상태를 삭제할 수 있습니다.
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 옵션
)
)
);