Files
nuxt-deep/docs/curriculum/05-state-management.md

5.1 KiB

5. 상태관리 전략

세 가지 선택지

방법 범위 언제 사용
Local State (ref, reactive) 컴포넌트 내부 해당 컴포넌트만 사용하는 UI 상태
Composable (useState) 여러 컴포넌트 공유 경량 전역 상태, 인증 정보 등
Pinia 앱 전체 복잡한 비즈니스 로직, 대규모 앱

Local State — 컴포넌트 내부 상태

<script setup>
// 컴포넌트에서만 쓰는 상태
const isOpen = ref(false)
const formData = reactive({
  name: '',
  price: 0
})

function toggle() {
  isOpen.value = !isOpen.value
}
</script>

적합한 경우:

  • 모달 열림/닫힘
  • 폼 입력값
  • 로컬 필터/정렬 상태

Composable (useState) — 경량 전역 상태

useState는 Nuxt 전용으로, SSR에서 서버/클라이언트 간 상태를 안전하게 공유한다. ref와 달리 SSR payload에 포함되어 Hydration 시 상태가 유지된다.

// composables/useAuth.ts
export function useAuth() {
  // 첫 번째 인자는 고유 키 (SSR payload 직렬화에 사용됨)
  const user = useState('auth-user', () => null)

  async function login(email: string, password: string) {
    const data = await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email, password }
    })
    user.value = data.user
  }

  function logout() {
    user.value = null
  }

  const isLoggedIn = computed(() => !!user.value)

  return { user, isLoggedIn, login, logout }
}
<!-- 어떤 컴포넌트에서든 같은 상태를 공유 -->
<script setup>
const { user, isLoggedIn, logout } = useAuth()
</script>

Pinia — 대규모 앱의 전역 상태

Nuxt에서 Pinia는 @pinia/nuxt 모듈로 자동 연동된다.

Store 정의

// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  // State
  const items = ref<CartItem[]>([])

  // Getters (computed)
  const totalCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  // Actions
  function addItem(product: Product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId: string) {
    items.value = items.value.filter(i => i.id !== productId)
  }

  async function checkout() {
    await $fetch('/api/orders', {
      method: 'POST',
      body: { items: items.value }
    })
    items.value = []
  }

  return { items, totalCount, totalPrice, addItem, removeItem, checkout }
})

Store 사용

<script setup>
const cart = useCartStore()

// storeToRefs로 반응성을 유지하며 구조분해
const { items, totalCount, totalPrice } = storeToRefs(cart)
// 액션은 그냥 구조분해 가능
const { addItem, removeItem } = cart
</script>

<template>
  <button @click="addItem(product)">
    장바구니 담기 ({{ totalCount }})
  </button>
</template>

전역 UI 상태 — 모달/토스트 패턴

// composables/useModal.ts
export function useModal() {
  const isOpen = useState('modal-open', () => false)
  const modalContent = useState<string | null>('modal-content', () => null)

  function open(content: string) {
    modalContent.value = content
    isOpen.value = true
  }

  function close() {
    isOpen.value = false
    modalContent.value = null
  }

  return { isOpen, modalContent, open, close }
}
// composables/useToast.ts — Nuxt UI의 useToast 활용
export function useAppToast() {
  const toast = useToast()

  function success(message: string) {
    toast.add({
      title: '성공',
      description: message,
      color: 'green'
    })
  }

  function error(message: string) {
    toast.add({
      title: '오류',
      description: message,
      color: 'red'
    })
  }

  return { success, error }
}

인증 상태 관리

프로젝트에서 Supabase를 사용할 때의 패턴:

// composables/useAuth.ts
export function useAuth() {
  const client = useSupabaseClient()
  const user = useSupabaseUser()  // @nuxtjs/supabase 제공

  const isLoggedIn = computed(() => !!user.value)

  async function signInWithEmail(email: string) {
    await client.auth.signInWithOtp({ email })
  }

  async function signOut() {
    await client.auth.signOut()
    navigateTo('/login')
  }

  return { user, isLoggedIn, signInWithEmail, signOut }
}

전략 선택 가이드

이 상태를 다른 컴포넌트에서도 쓰나?
  ├── 아니오 → Local State (ref, reactive)
  └── 예 →
        복잡한 로직이나 비동기 작업이 있나?
          ├── 예 → Pinia Store
          └── 아니오 → Composable (useState)

흔한 패턴:

  • 모달 열림/닫힘 → Local State
  • 사용자 정보, 인증 → Composable (useAuth)
  • 장바구니, 알림 목록 → Pinia
  • 테마, 언어 설정 → Composable (useState + localStorage)