모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

25강 / 전체 30강

성능 기초 — memo

7분 읽기 조회 5

Zustand로 전역 상태를 단순하고 강력하게 관리하는 방법, 스토어 설계 패턴, devtools 연동, 슬라이스 패턴까지 실전 Context 대안을 배웁니다.

🤷 Context의 한계와 Zustand의 등장

🤷 Context의 한계와 Zustand의 등장

Context API는 테마, 언어처럼 드물게 바뀌는 전역 값에 적합합니다. 하지만 자주 바뀌는 상태를 Context에 넣으면 해당 Context를 소비하는 모든 컴포넌트가 리렌더링되는 성능 문제가 생깁니다. 또한 코드 구조가 Provider 중첩으로 복잡해집니다.

Zustand(주스탄드)는 이 문제를 단순하게 해결하는 상태 관리 라이브러리입니다. 독일어로 "상태"를 의미합니다. Provider 없이, 보일러플레이트 없이, 필요한 값만 구독해 그것이 바뀔 때만 리렌더링합니다.

npm install zustand

🏗️ 스토어 만들기

// store/useCartStore.ts
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

interface CartStore {
  items: CartItem[]
  isOpen: boolean
  // 액션
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: number) => void
  updateQuantity: (id: number, quantity: number) => void
  clearCart: () => void
  toggleCart: () => void
  // 파생 값 (getters)
  totalPrice: () => number
  totalCount: () => number
}

export const useCartStore = create<CartStore>()(
  devtools(
    (set, get) => ({
      items: [],
      isOpen: false,

      addItem: (newItem) => set((state) => {
        const existing = state.items.find((i) => i.id === newItem.id)
        if (existing) {
          return {
            items: state.items.map((i) =>
              i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
            ),
          }
        }
        return { items: [...state.items, { ...newItem, quantity: 1 }] }
      }),

      removeItem: (id) => set((state) => ({
        items: state.items.filter((i) => i.id !== id),
      })),

      updateQuantity: (id, quantity) => set((state) => ({
        items: quantity <= 0
          ? state.items.filter((i) => i.id !== id)
          : state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
      })),

      clearCart: () => set({ items: [] }),
      toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),

      totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
      totalCount: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
    }),
    { name: 'CartStore' }  // devtools 표시명
  )
)

🔌 컴포넌트에서 사용하기

Provider 설정 없이 바로 import해서 사용합니다:

// 필요한 값만 선택해서 구독 (성능 최적화의 핵심)
function CartButton() {
  // totalCount만 구독 — items가 바뀌어도 totalCount가 같으면 리렌더 없음
  const totalCount = useCartStore((state) => state.totalCount())
  const toggleCart = useCartStore((state) => state.toggleCart)

  return (
    <button onClick={toggleCart} className="relative p-2">
      🛒
      {totalCount > 0 && (
        <span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
          {totalCount}
        </span>
      )}
    </button>
  )
}

function ProductCard({ product }: { product: { id: number; name: string; price: number } }) {
  const addItem = useCartStore((state) => state.addItem)

  return (
    <div className="border rounded-xl p-4">
      <h3 className="font-bold">{product.name}</h3>
      <p className="text-gray-500">{product.price.toLocaleString()}원</p>
      <button
        onClick={() => addItem(product)}
        className="mt-3 w-full py-2 bg-indigo-600 text-white rounded-lg text-sm"
      >
        장바구니 추가
      </button>
    </div>
  )
}

🍰 슬라이스 패턴 — 스토어 분리

스토어가 커지면 파일을 나눠서 슬라이스(slice)로 관리합니다:

// store/slices/authSlice.ts
import { StateCreator } from 'zustand'

export interface AuthSlice {
  user: { id: string; name: string } | null
  login: (user: AuthSlice['user']) => void
  logout: () => void
}

export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
})

// store/useStore.ts — 여러 슬라이스 합치기
import { create } from 'zustand'
import { createAuthSlice, AuthSlice } from './slices/authSlice'
import { createCartSlice, CartSlice } from './slices/cartSlice'

export const useStore = create<AuthSlice & CartSlice>()((...a) => ({
  ...createAuthSlice(...a),
  ...createCartSlice(...a),
}))

📊 Context vs Zustand 비교

기준Context APIZustand
설정 코드Provider, createContextcreate() 한 줄
리렌더링소비자 전체 리렌더구독한 값이 바뀔 때만
외부 접근훅 안에서만훅 + 모듈 직접 접근 가능
DevTools없음Redux DevTools 지원
러닝커브낮음낮음 (더 단순함)

✅ 정리

  • Zustand: Provider 없이 create()로 스토어 정의, 바로 사용.
  • 필요한 값만 선택적으로 구독 → 불필요한 리렌더 방지.
  • devtools 미들웨어로 Redux DevTools에서 상태 변화를 추적합니다.
  • 자주 바뀌는 전역 상태에서는 Context 대신 Zustand를 사용하세요.

관련 주제

  • React.memo
  • useMemo
  • useCallback
  • 불필요한 리렌더링 방지
  • 과용 주의
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...