Vuex를 이용한 Vue 전역 상태 관리가 궁금하다면?
2025.11.20 - [Web/Vue] - Vuex를 이용한 Vue 전역 상태 관리 예제
Vuex를 이용한 Vue 전역 상태 관리 예제
Vuex가 무엇인지, Vuex를 이용해 Vue 전역 상태 관리하는 방법이 궁금하다면?2025.11.20 - [Web/Vue] - Vuex 기초: Vue 전역 상태 관리 (state, getters, mutations, actions, modules) Vuex 기초: Vue 전역 상태 관리 (state, gett
sproutinghye.tistory.com
1. 서론
Vue 2 시절에는 Vuex가 사실상 표준 전역 상태 관리 라이브러리였습니다.
하지만 Vuex 3에 들어와서는 분위기가 조금 바뀌었습니다.
Pinia는 Vue에서 공식적으로 추천하는 새로운 상태 관리 라이브러리이고
Vue 3의 Composition API, TypeScript, DevTools와 잘 맞도록 설계되었습니다.
정리하자면, 새로운 Vue 3 프로젝트를 시작한다면
전역 상태 관리는 Vuex보다 Pinia를 우선 고려하는 게 자연스럽습니다.
이번 글에서는 Pinia를 설치해 기본적인 store를 만들고
Vue 컴포넌트에서 state/getter/actions를 쓰는 패턴까지 보겠습니다.
2. Pinia 설치 및 기본 세팅
설치
Vue 3 + Vite 프로젝트 기준으로 아래 명령어를 사용합니다.
npm install pinia
# 또는
yarn add pinia
Pinia는 Vue 2용 플러그인도 있지만, 여기에선 Vue 3 기준으로만 보겠습니다.
main.js에 Pinia 등록
main.js에서 Pinia를 생성하고 Vue 앱에 연결합니다.
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(router)
app.use(pinia)
app.mount('#app')
이렇게 하면 모든 컴포넌트에서 useXXXStore() 같은 함수를 통해 store에 접근할 준비가 됩니다.
3. 기본 개념
Vuex에서 state / getters / mutations / actions로 나누던 것과 비슷하게,
하나의 store가 특정 도메인의 상태와 로직을 관리합니다.
예를 들어,
- useCounterStore – 카운터, 숫자 관련 상태
- useUserStore – 로그인 상태, 사용자 정보
- useTodoStore – Todo 목록, 필터 상태
이런 식으로 도메인별 store를 여러 개 만들 수 있습니다.
4. 예제1 - useCounterStore 만들기
store 정의
보통 src/stores 폴더를 하나 만들고, 그 안에 store 파일을 둡니다.
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 1) state: 전역에서 공유할 데이터
state: () => ({
count: 0
}),
// 2) getters: 계산된 값 (computed 느낌)
getters: {
doubleCount(state) {
return state.count * 2
}
},
// 3) actions: 상태 변경 로직 (비동기도 포함)
actions:{
increment() {
this.count++
},
reset() {
this.count = 0
}
}
})
- defineStore('counter', { ... })
- 'counter'는 store의 id (DevTools에서 보이기도 함)
- state/getters/actions를 옵션 객체로 정의
여기서 tihs는 해당 store 인스턴스를 가리키기 때문에, this.count++처럼 바로 상태를 변경할 수 있습니다.
(Vuex처럼 mutation을 따로 만들 필요가 없습니다.)
컴포넌트에서 store 사용하기
<!-- CounterView.vue -->
<template>
<section>
<h1>Pinia Counter</h1>
<p>count: {{ counter. count }}</p>
<p>doubleCount: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
<button @click="counter.reset">Reset</button>
</section>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
- useCounterStore()를 호출하면 해당 store 인스턴스를 가져옵니다.
- counter.count, counter.doubleCount, counter.increment()처럼 state/getters/actions에 접근 가능합니다.
- Composition API 스타일로 자연스럽게 사용할 수 있습니다.
5. 예제2 - Todo store
이번에는 구조를 조금 확장한 패턴을 봐보겠습니다.
실무에서 더 자주 쓰게 될 구조는 목록 + 비동기 로딩 패턴입니다.
간단한 Todo 예시로 Pinia 패턴을 잡아볼게요.
Todo store 정의
// src/stores/todo.js
import { defineStore } from 'pinia'
import axios from 'axios'
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: [],
loading: false,
error: null,
filter: 'all' // all, done, pending
}),
getters: {
filteredTodos(state) {
if (state.filter === 'done') {
return state.todos.filter(t => t.completed)
}
if (state.filter === 'pending') {
return state.todos.filter(t => !t.completed)
}
return state.todos
},
doneCount(state) {
return state.todos.filter(t => t.completed).length
}
},
actions: {
async fetchTodos() {
this.loading = true
this.error = null
try {
const res = await axios.get(
'https://jsonplaceholder.typicode.com/todos?_limit=5'
)
this.todos = res.data
} catch (e) {
this.error = '할 일을 불러오지 못했습니다.'
} finally {
this.loading = false
}
},
toggleTodo(id) {
const target = this.todos.find(t => t.id === id)
if (target) {
target.completed = !target.completed
}
},
changeFilter(nextFilter) {
this.filter = nextFilter
}
}
})
컴포넌트에서 사용
<!-- TodoView.vue -->
<template>
<section>
<h1>Pinia Todo</h1>
<div>
<button @click="todo.fetchTodos" :disabled="todo.loading">
{{ todo.loading ? '로딩 중...' : '불러오기' }}
</button>
<span v-if="todo.error">{{ todo.error }}</span>
</div>
<div>
<button @click="todo.changeFiilter('all')">전체</button>
<button @click="todo.changeFiilter('done')">완료</button>
<button @click="todo.changeFiilter('pending')">미완료</button>
</div>
<p>완료된 할 일: {{ todo.doneCount }}개</p>
<ul>
<li
v-for="t in todo.filteredTodos"
:key="t.id"
:style="{ textDecoration: t.completed ? 'line-through' : 'none' }"
>
<label>
<input
type="checkbox"
:checked="t.completed"
@change="todo.toggleTodo(t.id)"
/>
{{ t.title }}
</label>
</li>
</ul>
</section>
</template>
<script setup>
import { useTodoStore } from '@/stores/todo'
const todo = useTodoStore()
</script>
이 예제를 기준으로 보면, Pinia store가 사실상 "Vuex의 state + getters + actions"를 합친 것처럼 느껴집니다.
6. Vuex와 Pinia의 차이
선언 방식
- Vuex
- new Vuex.Store({ state, mutations, actions, getters, modules })
- Pinia
- defineStore('id', { state, getters, actions })
- store를 여러 개 만들고, 필요할 때 useXXXStore()로 가져다 쓰는 방식
Pinia는 여러 개의 작은 store를 만드는 것을 기본값으로 삼고 있습니다.
mutations 유무
- Vuex: mutations를 통해서만 state 변경 (동기)
- Pinia: actions 안에서 this.xxx를 바로 수정 (mutations 없고, 동기/비동기 둘 다 actions)
Pinia에서는 mutations 개념이 사라져서 코드가 훨씬 단순해지고,
TypeScript 지원이나 IDE 자동완성도 더 잘 됩니다.
Composition API와의 궁합
Vue 3 기준에서 Vuex도 쓸 수 있지만, Pinia의 구조 자체가 Options API 스타일에 더 가깝고,
Pinia는 아예 Composition API/<script setup>과 함께 쓰도록 설계되어 있습니다.
그래서 새로운 Vue 3 프로젝트에서는 Pinia 쪽이 자연스럽게 느껴질 가능성이 큽니다.
7. 정리
- Pinia는 Vue에서 권장하는 Vue3용 상태 관리 라이브러리
- createPinia()를 만들어 app.use(pinia)로 앱에 등록
- defineStore('id', { state, getters, actions })로 store 정의
- 컴포넌트에서는 const store = useXXXStore()로 사용
- Vuex처럼 mutations를 따로 만들지 않고, actions에서 바로 this.state를 수정
여기까지 익혀둔다면 Router 4 + Pinia + Composable을 이용해
로그인, 게시글 목록, 필터링 같은 실전 수준의 구조를 손으로 짤 준비가 됩니다.
출처
Pinia 공식 문서 소개 - Pinia란?, 시작하기
Pinia 공식 문서 핵심 개념 - 스토어 정의하기, 상태(State), 게터(Getters), 액션(Actions), 플러그인(Plugins), 컴포넌트 외부에서의 스토어 사용
'Framework > Vue' 카테고리의 다른 글
| Vue 3 + Router 4 + Pinia로 미니 SPA 만들기 (0) | 2025.11.26 |
|---|---|
| Vue3 <script setup> 정리: defineProps, defineEmits, defineExpose (0) | 2025.11.26 |
| Vue Router 3 vs 4 비교: Vue2에서 Vue3로 라우터 마이그레이션 (0) | 2025.11.25 |
| Vue 2 vs Vue 3 비교: Options API에서 Composition API로 마이그레이션 (0) | 2025.11.25 |
| Vue 3 Composable 패턴 이해하기: userCounter, useFetch로 로직 재사용하기 (0) | 2025.11.24 |