Skip to content

반응성 패턴

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()의 옵션은 다음과 같습니다.

옵션타입설명
detachedbooleantrue로 설정하면 컴포넌트 언마운트 후에도 구독이 유지됩니다.
deepboolean중첩 객체의 변경까지 감지합니다. 기본값은 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 파일 내에 정의하며, 매 호출 시 새로운 객체를 반환해야 합니다.

TIENIPIA QUALIFIED STANDARD