← 가이드 목록으로 ← Back to guides

Vue 3 Composition API와 상태 관리 완벽 가이드 Vue 3 Composition API & State Management Complete Guide

Vue 3의 Composition API는 로직 재사용과 코드 구성을 혁신적으로 개선합니다. Options API의 한계를 넘어, Composables 패턴과 Pinia를 활용한 모던 Vue 개발 방법을 배워봅시다.

Vue 3's Composition API revolutionizes logic reuse and code organization. Going beyond Options API limitations, let's learn modern Vue development with Composables patterns and Pinia.

ref vs reactive 제대로 이해하기 Understanding ref vs reactive Properly

// ref: 원시값과 객체 모두 가능, .value로 접근
import { ref } from 'vue'

const count = ref(0)
const user = ref({ name: '김철수', age: 25 })

// 접근 시 .value 필요
count.value++
user.value.name = '이영희'

// template에서는 .value 불필요
<template>
  <p>{{ count }}</p> <!-- .value 자동 언래핑 -->
</template>
// ref: Works for primitives and objects, access with .value
import { ref } from 'vue'

const count = ref(0)
const user = ref({ name: 'John', age: 25 })

// Need .value when accessing
count.value++
user.value.name = 'Jane'

// No .value needed in template
<template>
  <p>{{ count }}</p> <!-- .value auto-unwrapped -->
</template>
// reactive: 객체 전용, .value 불필요
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: '김철수' }
})

// 직접 접근 가능
state.count++
state.user.name = '이영희'
// reactive: Objects only, no .value needed
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'John' }
})

// Direct access possible
state.count++
state.user.name = 'Jane'

⚠️ reactive 주의사항 ⚠️ reactive Caveats

reactive 객체를 구조분해하면 반응성이 사라집니다. toRefs()를 사용하거나, ref를 선호하세요.

Destructuring a reactive object loses reactivity. Use toRefs() or prefer ref.

Composables 패턴으로 로직 재사용 Reusing Logic with Composables Pattern

Composables는 Composition API를 활용한 상태 로직 재사용 패턴입니다. 컴포넌트 간에 공유할 로직을 독립적인 함수로 추출합니다.

Composables are a pattern for reusing stateful logic with Composition API. Extract shareable logic between components into independent functions.

// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  const fetchData = async () => {
    loading.value = true
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, fetchData }
}
// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  const fetchData = async () => {
    loading.value = true
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, fetchData }
}
// 컴포넌트에서 사용
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data, loading, error, fetchData } = useFetch('/api/users')
fetchData()
</script>
// Using in component
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data, loading, error, fetchData } = useFetch('/api/users')
fetchData()
</script>

Pinia로 상태 관리하기 State Management with Pinia

Pinia는 Vue 3의 공식 상태 관리 라이브러리입니다. Vuex보다 간단하고 TypeScript 지원이 뛰어납니다.

Pinia is the official state management library for Vue 3. Simpler than Vuex with excellent TypeScript support.

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  // state
  state: () => ({
    name: '',
    isLoggedIn: false
  }),

  // getters (computed와 유사)
  getters: {
    greeting: (state) => `안녕하세요, ${state.name}님!`
  },

  // actions (동기/비동기 모두 가능)
  actions: {
    async login(username, password) {
      const res = await api.login(username, password)
      this.name = res.name
      this.isLoggedIn = true
    },
    logout() {
      this.$reset() // state 초기화
    }
  }
})
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  // state
  state: () => ({
    name: '',
    isLoggedIn: false
  }),

  // getters (similar to computed)
  getters: {
    greeting: (state) => `Hello, ${state.name}!`
  },

  // actions (sync/async both possible)
  actions: {
    async login(username, password) {
      const res = await api.login(username, password)
      this.name = res.name
      this.isLoggedIn = true
    },
    logout() {
      this.$reset() // Reset state
    }
  }
})

성능 최적화: v-memo와 KeepAlive Performance Optimization: v-memo and KeepAlive

<!-- v-memo: 조건이 변경될 때만 재렌더링 -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
  <p>{{ item.name }}</p>
  <p>{{ item.selected ? '선택됨' : '선택 안됨' }}</p>
</div>

<!-- KeepAlive: 컴포넌트 상태 유지 -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

<!-- 특정 컴포넌트만 캐시 -->
<KeepAlive :include="['TabA', 'TabB']" :max="10">
  <component :is="currentTab" />
</KeepAlive>
<!-- v-memo: Only re-render when condition changes -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
  <p>{{ item.name }}</p>
  <p>{{ item.selected ? 'Selected' : 'Not selected' }}</p>
</div>

<!-- KeepAlive: Preserve component state -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

<!-- Cache specific components only -->
<KeepAlive :include="['TabA', 'TabB']" :max="10">
  <component :is="currentTab" />
</KeepAlive>

💡 KeepAlive 라이프사이클 💡 KeepAlive Lifecycle

KeepAlive로 감싼 컴포넌트는 onActivatedonDeactivated 훅을 사용할 수 있습니다. 캐시된 컴포넌트가 다시 활성화될 때 데이터를 갱신하는 데 유용합니다.

Components wrapped with KeepAlive can use onActivated and onDeactivated hooks. Useful for refreshing data when cached components become active again.