군더더기 없는 Vue 상태 관리,
Pinia
Vuex의 복잡한 보일러플레이트를 과감히 버리고 Composition API의 철학을 온전히 담은 Vue 공식 상태 관리 라이브러리. 간결하고, 타입 안전하며, 직관적이다.
Vue 2 시절 공식 상태 관리 도구는 Vuex였다. 작동은 했지만, mutations와 actions의 이중 구조, 장황한 보일러플레이트, 미흡한 TypeScript 지원이 늘 불편했다. Pinia는 그 모든 불편함을 해소하며 Vue 공식 상태 관리 라이브러리 자리를 넘겨받았다.
Vuex vs Pinia
| 항목 | Vuex 4 | Pinia |
|---|---|---|
| Mutations 필요 여부 | ✗ 필수 | ✓ 불필요, actions만 사용 |
| TypeScript 지원 | ✗ 추가 설정 필요 | ✓ 완전 지원 (타입 추론 자동) |
| DevTools | ✓ 지원 | ✓ 지원 (time-travel 포함) |
| 모듈 구조 | 중첩 모듈, 네임스페이스 | 스토어 파일 분리, 자동 코드 분할 |
| 번들 크기 | ~10KB | ~1.5KB (gzip) |
스토어의 세 가지 구성 요소
Pinia 스토어는 세 가지 개념으로 구성된다.
ref() 또는 reactive()로 정의한다.
computed와 동일. 캐싱이 자동으로 적용된다.
스토어 정의하기
Pinia는 두 가지 방식으로 스토어를 정의할 수 있다. Options 방식과 Setup 방식이다.
Options 방식 (Vuex에 익숙한 경우)
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: '카운터',
}),
getters: {
doubleCount: (state) => state.count * 2,
label() {
return `${this.name}: ${this.count}`
}
},
actions: {
increment() {
this.count++
},
async fetchAndSet() {
const data = await fetchCount()
this.count = data.value
}
}
})
Setup 방식 (Composition API 스타일, 권장)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state → ref / reactive
const count = ref(0)
const name = ref('카운터')
// getters → computed
const doubleCount = computed(() => count.value * 2)
const label = computed(() => `${name.value}: ${count.value}`)
// actions → 일반 함수
function increment() { count.value++ }
async function fetchAndSet() {
const data = await fetchCount()
count.value = data.value
}
return { count, name, doubleCount, label, increment, fetchAndSet }
})
Setup 방식 권장: Composition API에 익숙하다면 Setup 방식이 훨씬 자연스럽다. Composable을 스토어 안에서 직접 사용할 수도 있고, 외부 Composable과의 통합도 쉽다.
컴포넌트에서 스토어 사용하기
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// storeToRefs: 반응성 유지하며 구조 분해
const { count, doubleCount, label } = storeToRefs(store)
// actions는 직접 구조 분해 가능 (반응성 불필요)
const { increment, fetchAndSet } = store
</script>
<template>
<p>{{ label }}</p>
<p>두 배: {{ doubleCount }}</p>
<button @click="increment">+1</button>
</template>
storeToRefs 주의: 스토어를 구조 분해할 때 반응성이 사라지는 문제를 막으려면 storeToRefs()를 사용해야 한다. actions는 함수이므로 storeToRefs 없이 바로 꺼내도 된다.
스토어 간 통신
Pinia에서는 스토어끼리 서로를 자유롭게 참조할 수 있다. Vuex처럼 루트 스토어를 거치지 않아도 된다.
// stores/order.js — auth 스토어를 참조
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
export const useOrderStore = defineStore('order', () => {
async function placeOrder(items) {
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
throw new Error('로그인이 필요합니다')
}
await submitOrder(authStore.userId, items)
}
return { placeOrder }
})
플러그인과 확장
Pinia는 플러그인 시스템을 통해 기능을 확장할 수 있다. 로컬스토리지 동기화, 로깅, 에러 트래킹 등을 스토어 전체에 일괄 적용할 수 있다.
pinia-plugin-persistedstate
스토어 상태를 localStorage나 sessionStorage에 자동 저장/복원. 새로고침 후에도 상태 유지.
Vue DevTools 통합
각 스토어의 상태 변화를 타임라인으로 추적. 액션 단위의 time-travel 디버깅 지원.
Pinia는 작은 크기(~1.5KB), 직관적인 API, 완전한 TypeScript 지원, Composition API와의 자연스러운 통합으로 Vue 3 프로젝트에서 사실상 표준 상태 관리 도구가 되었다. Vuex를 쓰고 있다면 마이그레이션을 적극 고려할 만하다.
댓글