THE DEVLOG

scribbly.

Next JS 치트 시트

2025.05.14 07:46:05

Zustand

Zustand는 독일어로 '상태(state)'를 의미한다.
React에서 사용할 수 있는 가볍고 간단한 상태 관리 라이브러리다.

Zustand는 React의 렌더 사이클과 훅 기반으로 작동한다.
observable 객체를 사용하는 다른 상태관리 도구(MobX 등)보다 observing이 약하지만 단순하고 React와 잘 통합된다는 장점이 있다.

핵심 개념

구분설명
Store앱 전체에서 공유할 상태와 로직을 정의한 공간
createstore를 생성하는 함수
useStore()store에서 상태를 읽고 쓰기 위한 hook
set()store 상태 변경
subscribe()상태 변경을 구독 (옵셔널)

사용법

스토어 생성

import { create } from 'zustand'

// 상태 생성
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

상태 사용 (컴포넌트에서)

function Counter() {
  const count = useStore((state) => state.count)
  const increment = useStore((state) => state.increment)

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
    </div>
  )
}

고급 패턴

선택적 구독 (성능 최적화)

필요한 속성만 구독 가능 → 불필요한 리렌더 방지.

const count = useStore((state) => state.count)

상태 외부에서 직접 접근

useStore.getState().increment()

상태 외부에서 구독

const unsub = useStore.subscribe(
  (state) => state.count,
  (count) => console.log('count 변경:', count)
)

특징 정리

특징설명
Hook 기반React의 useState 느낌 그대로 사용
복잡한 설정 필요 없음Redux 같은 boilerplate 없음
컴포넌트 외부에서도 접근 가능store.getState(), store.setState() 사용 가능
불변성 관리 자동화immer 사용 없이도 안전하게 상태 업데이트 가능
선택적 구독 가능selector 사용으로 리렌더 최적화 가능

미들웨어

Zustand는 Redux처럼 미들웨어를 플러그인처럼 사용할 수 있음.
예:

  • zustand/middlewaredevtools, persist, immer
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

const useStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
      }),
      { name: 'my-store' }
    )
  )
)

요약

  • create(set => { ... }) 로 store 생성
  • useStore((state) => state.xxx) 로 상태 사용
  • set() 으로 상태 변경
  • React 외부에서도 useStore.getState() 가능
  • 선택적 구독, 구독자 직접 관리 가능
  • 미들웨어로 persist, devtools, immer 확장 가능

Zustand Context Provider와 SSR

초기 상태와 함께 Zustand Context Provider 초기화(공식문서)
Next.js로 설정(공식문서)

Zustand는 기본적으로 전역 변수를 사용하나, Next.js의 hydration 에러를 해결하기 위해 Context를 사용하여 SSR 친화적인 환경을 만들 수 있다.

 

먼저 아래와 같이 Store 객체를 만든다. (use-auth-store.tsx)

import { createStore } from "zustand";
import { Session, User } from "@supabase/supabase-js";
import { createClient } from "@/utils/supabase/client";

const supabase = createClient();

export interface AuthState {
  user: User | null;
  loading: boolean;
  setUser: (user: User | null, session: Session | null) => void;
}

export const createAuthStore = (initialState?: Partial<AuthState>) =>
  createStore<AuthState>((set) => ({
    user: null,
    loading: true, 
    setUser: (user, session) =>
      set({
        user,
        loading: false,
      }),
    ...initialState, // 초기값 덮어쓰기
  }));

 

먼저 아래와 같이 Store 객체를 만든다. (use-auth-store.tsx)

import { createStore } from "zustand";
import { Session, User } from "@supabase/supabase-js";
import { createClient } from "@/utils/supabase/client";

const supabase = createClient();

export interface AuthState {
  user: User | null;
  loading: boolean;
  setUser: (user: User | null, session: Session | null) => void;
}

export const createAuthStore = (initialState?: Partial<AuthState>) =>
  createStore<AuthState>((set) => ({
    user: null,
    loading: true, 
    setUser: (user, session) =>
      set({
        user,
        loading: false,
      }),
    ...initialState, // 초기값 덮어쓰기
  }));

 

아래와 같이 StoreProvider, useStore를 선언한다.

"use client";

import { createContext, useRef, useContext, ReactNode } from "react";
import { useStore } from "zustand";
import { createAuthStore, AuthState } from "@/hooks/zustand/use-auth-store";

export type AuthStoreApi = ReturnType<typeof createAuthStore>;

const AuthStoreContext = createContext<AuthStoreApi | undefined>(undefined);

interface AuthStoreProviderProps {
  children: ReactNode;
  initialState?: Partial<AuthState>;
}

export const AuthStoreProvider = ({
  children,
  initialState,
}: AuthStoreProviderProps) => {
  const storeRef = useRef<AuthStoreApi>(null);

  if (!storeRef.current) {
    storeRef.current = createAuthStore(initialState);
  }

  return (
    <AuthStoreContext.Provider value={storeRef.current}>
      {children}
    </AuthStoreContext.Provider>
  );
};

export const useAuthStore = <T,>(selector: (store: AuthState) => T): T => {
  const authStoreContext = useContext(AuthStoreContext);

  if (!authStoreContext) {
    throw new Error("useAuthStore must be used within AuthStoreProvider");
  }

  return useStore(authStoreContext, selector);
};

 

해당 useStore를 사용하려는 범위에서 storeProvider로 감싼다.

    <html lang="ko-KR">
      <body
        className={`antialiased h-screen flex flex-col bg-background text-color-base font-sans scrollbar-hidden`}
      >
        <AuthStoreProvider>
              {children}
        </AuthStoreProvider>
      </body>
    </html>