"그저 쉬운 라이브러리?" 실무에서 뽑아먹는 Zustand 고급 테크닉 5가지
3
공유

Zustand는 흔히 '러닝 커브가 낮다'고 평가받지만, 프로젝트가 커질수록 그 진가는 얼마나 정교하게 설계하느냐에 달려 있습니다. 단순히 setget을 쓰는 단계를 넘어, 성능 최적화와 아키텍처 관점에서의 고급 활용법을 공유합니다.


1. Selector 최적화: Atomic vs Object

많은 개발자가 실수하는 부분은 스토어 전체를 구조 분해 할당으로 가져오는 것입니다. 이는 불필요한 리렌더링의 주범입니다.

1 2 3 4 5 6 7 8 9 10 11 12 13 // ❌ Bad: 다른 상태가 변해도 리렌더링됨 const { bears, fish } = useStore(); // ✅ Good: 개별 상태만 구독 (Atomic Selector) const bears = useStore((state) => state.bears); const fish = useStore((state) => state.fish); // 🚀 Advanced: 얕은 비교(shallow)를 통한 객체 구독 import { shallow } from 'zustand/shallow' const { bears, fish } = useStore( (state) => ({ bears: state.bears, fish: state.fish }), shallow );

2. Slice Pattern: 대규모 스토어의 모듈화

스토어가 비대해지면 관리 지옥이 시작됩니다. Slice Pattern을 통해 도메인별로 상태를 분리하고, 이를 하나의 Bound Store로 결합하세요.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // userSlice.js export const createUserSlice = (set) => ({ user: null, updateUser: (newUser) => set({ user: newUser }), }); // cartSlice.js export const createCartSlice = (set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), }); // store.js (통합) import { create } from 'zustand'; export const useBoundStore = create((...a) => ({ ...createUserSlice(...a), ...createCartSlice(...a), }));

3. Transient Updates: 리액트를 거치지 않는 초고속 업데이트

애니메이션이나 마우스 트래킹처럼 1초에 수십 번 변하는 값은 리액트의 렌더링 사이클을 태우면 성능 저하가 발생합니다. subscribe를 활용해 비제어 방식으로 업데이트하세요.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const useGameStore = create(() => ({ score: 0 })); function ScoreDisplay() { const scoreRef = useRef(); useEffect(() => { // 컴포넌트 리렌더링 없이 DOM만 직접 조작 const unsub = useGameStore.subscribe( (state) => state.score, (score) => { if (scoreRef.current) scoreRef.current.innerText = score; } ); return unsub; }, []); return <div ref={scoreRef} />; }

4. Hook 밖에서 스토어 제어하기 (Vanilla JS Access)

Zustand의 강력함은 Non-React 환경에서도 동작한다는 점입니다. API 인터셉터나 유틸리티 함수에서 상태를 조회하거나 수정할 때 유용합니다.

1 2 3 4 5 6 7 8 9 10 11 12 // api.js import { useAuthStore } from './store'; axios.interceptors.request.use((config) => { // Hook이 아니어도 사용 가능! const { token } = useAuthStore.getState(); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // 필요 시 외부에서 값 변경도 가능 useAuthStore.setState({ token: null });

5. TypeScript와 함께하는 Action 분리 패턴

액션(함수)은 한 번 생성되면 변하지 않습니다. 상태와 액션을 분리 정의하면 타입 추론이 깔끔해지고 성능 최적화에도 유리합니다.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface State { count: number; } interface Actions { increment: (qty: number) => void; reset: () => void; } const useCounterStore = create<State & { actions: Actions }>((set) => ({ count: 0, actions: { increment: (qty) => set((state) => ({ count: state.count + qty })), reset: () => set({ count: 0 }), }, })); // 사용성 개선을 위한 커스텀 훅 export const useCount = () => useCounterStore((state) => state.count); export const useCounterActions = () => useCounterStore((state) => state.actions);

💡 마치며: 왜 Zustand인가?

Redux의 엄격함이 대규모 서비스의 안정성을 준다면, Zustand의 유연함은 빠른 이터레이션과 극한의 성능 최적화를 가능하게 합니다. 위 기법들을 적재적소에 활용하여 더 가볍고 빠른 리액트 앱을 구축해 보세요!

댓글을 작성하려면로그인이 필요합니다.

관련 글