Googling/React + Next + Eco

Zustand에서 `useStore` 값 꺼내 쓰는 다양한 패턴과 shallow 비교

날개단 2025. 10. 3. 23:11
728x90

Zustand를 쓰다 보면 상태 값과 setter(액션)를 어떻게 꺼내 쓰는 게 가장 효율적인지 고민이 생깁니다.
이번 글에서는 제가 정리한 값/세터 패턴, shallow 비교의 의미, 그리고 커스텀 훅으로 깔끔하게 묶는 방법을 공유합니다.


1. 값 꺼내기: 3가지 방식

const { viewMode } = useUIStore();
const viewMode2 = useUIStore((s) => s.viewMode);
const { viewMode: viewMode3 } = useUIStore.getState();

(1) `const { viewMode } = useUIStore();`

  • 스토어 전체 구독 → 어떤 값이든 바뀌면 리렌더
  • 단순하지만 불필요한 리렌더 위험

(2) `const viewMode2 = useUIStore((s) => s.viewMode);`

  • selector로 필요한 값만 구독
  • 값이 바뀔 때만 리렌더 → 가장 권장되는 방식

(3) `const { viewMode: viewMode3 } = useUIStore.getState();`

  • 구독 없이 현재 값 스냅샷만 읽음
  • 값이 바뀌어도 리렌더 안 됨
  • 이벤트 핸들러, 비동기 로직에서만 사용

2. 여러 값 구독 시 shallow 비교

import { shallow } from "zustand/shallow";

const { viewMode, theme } = useUIStore(
  (s) => ({ viewMode: s.viewMode, theme: s.theme }),
  shallow
);
  • `{...}` 객체는 매번 새로 생성 → shallow 없으면 리렌더 과다
  • shallow 비교는 1단계 값만 비교해서 불필요한 리렌더 막아줌
  • 💡 shallow를 쓰면 내부적으로 각각 셀렉터로 구독한 것과 동일한 효과입니다.
    즉, 아래와 같은 개별 구독과 리렌더 동작이 동일합니다 👇
const viewMode = useUIStore((s) => s.viewMode);
const theme = useUIStore((s) => s.theme);

3. 세터(액션) 뽑기

세터는 함수 참조가 변하지 않지만,
`useUIStore()` 빈 호출로 구조분해하면 스토어 전체를 구독하게 되어 손해입니다.

👉 따라서 세터도 셀렉터로 뽑는 게 가장 안전합니다.

// 권장
const setViewMode = useUIStore((s) => s.setViewMode);

// 일회성 (이벤트 핸들러 등)
useUIStore.getState().setViewMode("dark");

4. 커스텀 훅으로 깔끔하게 묶기

스토어가 여러 개이거나, 셀렉터 호출이 많아지면
하나의 커스텀 훅으로 정리하는 것이 좋습니다.

예를 들어 UI, 테마, 사용자 스토어가 각각 있다면:

// useAppUI.ts
import { shallow } from "zustand/shallow";
import { useUIStore } from "@/stores/ui";
import { useThemeStore } from "@/stores/theme";
import { useUserStore } from "@/stores/user";

export function useAppUI() {
  const viewMode = useUIStore((s) => s.viewMode);
  const theme = useThemeStore((s) => s.theme);
  const user = useUserStore((s) => s.user);
  const setViewMode = useUIStore((s) => s.setViewMode);

  return { viewMode, theme, user, setViewMode };
}

이제 컴포넌트에서는 간단히 사용합니다:

const { viewMode, theme, user, setViewMode } = useAppUI();

✅ 장점

  • import가 간결해짐
  • 각 스토어 구독은 여전히 개별 셀렉터 단위 → 리렌더 영향 없음
  • 스토어 구조 변경 시 훅 내부만 수정하면 됨

💡 shallow는 한 스토어 내 여러 속성을 묶을 때만 의미가 있고,
스토어 간엔 필요 없습니다. (각 스토어가 자체적으로 독립 구독을 관리하기 때문이에요.)


5. 추천 패턴 요약

상황 추천 방식
값 1개 `useUIStore((s) => s.value)`
값 여러 개 `useUIStore((s) => ({ a: s.a, b: s.b }), shallow)`
세터 `useUIStore((s) => s.setValue)`
비반응 스냅샷 `useUIStore.getState()`
여러 스토어 커스텀 훅(`useAppUI()`)으로 묶기

6. 결론

✅ 값은 selector로
✅ 여러 값은 selector + shallow로 (→ 각각 구독한 것과 동일 효과)
✅ 세터도 selector로
✅ 여러 스토어는 커스텀 훅으로 정리
✅ 일회성은 getState()


Zustand는 단순하지만 유연합니다.
핵심은 “최소 구독 + 명확한 의도”를 유지하는 것.
이 글이 여러분의 스토어 구조를 더 깔끔하게 정리하는 데 도움이 되길 바랍니다 🚀