반응성 패턴
13.3.1. storeToRefs
Store에서 상태와 getter를 구조 분해할 때는 반드시 storeToRefs()를 사용합니다. 일반 구조 분해를 사용하면 반응성이 소실됩니다.
- state, getter:
storeToRefs()로 구조 분해합니다. - action: 직접 구조 분해합니다. action은 일반 함수이므로
storeToRefs()가 불필요합니다.
typescript
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/useUserStore'
const userStore = useUserStore()
// state, getter는 storeToRefs로 추출합니다.
const { user, isLoggedIn, displayName } = storeToRefs(userStore)
// action은 직접 구조 분해합니다.
const { loadCurrentUser, logout } = userStore다음은 올바르지 않은 패턴입니다. 반응성이 소실되어 템플릿이 갱신되지 않습니다.
typescript
// 금지: 반응성이 소실됩니다.
const { user, isLoggedIn } = useUserStore()13.3.2. computed 활용
Store 내부에서 파생 상태는 computed()로 정의합니다. 컴포넌트에서 동일한 계산을 반복하지 않습니다.
typescript
// src/stores/useCartStore.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { CartItem } from '@/types/cart'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
// 파생 상태는 computed로 정의합니다.
const itemCount = computed(() => items.value.length)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const isEmpty = computed(() => items.value.length === 0)
return { items, itemCount, totalPrice, isEmpty }
})- 동일한 파생 로직을 여러 컴포넌트에서 사용하는 경우, 컴포넌트가 아닌 Store getter로 정의합니다.
- 컴포넌트에서 Store getter를 소비할 때는
storeToRefs()를 사용합니다.
typescript
// 컴포넌트에서 getter 소비
const cartStore = useCartStore()
const { totalPrice, isEmpty } = storeToRefs(cartStore)13.3.3. $subscribe
$subscribe()를 사용하여 Store 상태 변경을 감지할 수 있습니다.
typescript
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
// mutation.storeId: Store ID
// state: 변경 후 상태
localStorage.setItem('user', JSON.stringify(state.user))
})$subscribe()의 옵션은 다음과 같습니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
detached | boolean | true로 설정하면 컴포넌트 언마운트 후에도 구독이 유지됩니다. |
deep | boolean | 중첩 객체의 변경까지 감지합니다. 기본값은 true입니다. |
flush | 'pre' | 'post' | 'sync' | 콜백 실행 타이밍을 지정합니다. |
typescript
// 컴포넌트 언마운트 후에도 구독을 유지하는 패턴
userStore.$subscribe(
(mutation, state) => {
sessionStorage.setItem('user-state', JSON.stringify(state))
},
{ detached: true }
)detached: true는 앱 전역에서 상태를 영속화하는 경우에만 사용합니다.- 일반적인 컴포넌트 내 구독은 기본 옵션을 사용하여 자동 정리되도록 합니다.
13.3.4. $reset 패턴
Setup Store는 Options Store와 달리 $reset() 메서드가 자동 제공되지 않습니다. 상태 초기화가 필요한 경우 직접 구현해야 합니다.
typescript
// src/stores/useFormStore.ts
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useFormStore = defineStore('form', () => {
const name = ref('')
const email = ref('')
const agreed = ref(false)
// $reset을 대체하는 초기화 action을 정의합니다.
function resetState() {
name.value = ''
email.value = ''
agreed.value = false
}
return { name, email, agreed, resetState }
})- 모든 Setup Store에
resetState()action을 포함하는 것을 권장합니다. - 초기값이 복잡한 경우 팩토리 함수를 활용합니다.
typescript
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { OrderDraft } from '@/types/order'
function createInitialState(): OrderDraft {
return {
items: [],
shippingAddress: null,
paymentMethod: null,
memo: '',
}
}
export const useOrderDraftStore = defineStore('order-draft', () => {
const draft = ref<OrderDraft>(createInitialState())
function resetState() {
draft.value = createInitialState()
}
return { draft, resetState }
})- 초기값 팩토리 함수는 Store 파일 내에 정의하며, 매 호출 시 새로운 객체를 반환해야 합니다.