5.1 KiB
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)