Vue 컴포넌트는 이미 반응형 상태를 관리하고 있다.
상태 === 데이터
상태 (state) , 뷰 (View) , 기능 (Actions)
-> 단방향 데이터 흐름의 간단한 표현
여러 컴포넌트가 상태를 공유할 때
1. 여러 뷰가 동일한 상태에 종속되는 경우
2. 서로 다른 뷰의 기능이 동일한 상태(데이터)를 변경시켜야 하는 경우
계층 구조가 깊어질 경우 비효율적, 관리가 어려워진다!
각 컴포넌트의 공유 상태를 추출하여, 전역에서 참조할 수 있는 저장소에서 관리
Pinia의 등장!
Vue의 공식 상태 관리 라이브러리 === "Pinia"
store, state, getters, actions, plugin 으로 구성되어 있다.
중앙 저장소, 모든 컴포넌트가 공유하는 상태, 기능 등이 작성됨
반응형 상태(데이터), ref() === state
계산된 값, conputed() === getters
메서드, function() === actions
애플리케이션의 상태 관리에 필요한 추가 기능을 제공하거나 확장하는 도구
먼저 프로젝트에 Pinia를 설치:
npm install pinia
main.js에서 Pinia를 Vue 애플리케이션에 등록한다:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
stores 디렉토리를 만들고 첫 번째 스토어를 생성:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state
state: () => ({
count: 0,
name: 'Vue 상태 관리',
todos: [
{ id: 1, text: 'Pinia 배우기', done: false },
{ id: 2, text: 'Vue 프로젝트 만들기', done: true }
]
}),
// getters
getters: {
doubleCount: (state) => state.count * 2,
finishedTodos: (state) => state.todos.filter(todo => todo.done),
unfinishedTodos: (state) => state.todos.filter(todo => !todo.done),
getTodoById: (state) => {
return (id) => state.todos.find(todo => todo.id === id)
}
},
// actions
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(Math.random() * 100)
},
addTodo(text) {
const id = this.todos.length + 1
this.todos.push({ id, text, done: false })
},
toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id)
if (todo) {
todo.done = !todo.done
}
}
}
})
Pinia는 Vue 3의 컴포지션 API와 완벽하게 호환된다:
<!-- Counter.vue -->
<template>
<div>
<h3>카운터: {{ counter.count }}</h3>
<h3>두 배 값: {{ counter.doubleCount }}</h3>
<button @click="counter.increment()">증가</button>
<button @click="counter.randomizeCounter()">랜덤값</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter'
// 스토어 사용하기
const counter = useCounterStore()
</script>
<!-- TodoList.vue -->
<template>
<div>
<h3>할 일 목록</h3>
<form @submit.prevent="addNewTodo">
<input v-model="newTodo" placeholder="할 일 입력" />
<button type="submit">추가</button>
</form>
<h4>미완료 항목 ({{ todoStore.unfinishedTodos.length }}개)</h4>
<ul>
<li v-for="todo in todoStore.unfinishedTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.done"
@change="todoStore.toggleTodo(todo.id)"
/>
{{ todo.text }}
</li>
</ul>
<h4>완료된 항목 ({{ todoStore.finishedTodos.length }}개)</h4>
<ul>
<li v-for="todo in todoStore.finishedTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.done"
@change="todoStore.toggleTodo(todo.id)"
/>
<span style="text-decoration: line-through">{{ todo.text }}</span>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useCounterStore } from '../stores/counter'
const todoStore = useCounterStore()
const newTodo = ref('')
function addNewTodo() {
if (newTodo.value.trim()) {
todoStore.addTodo(newTodo.value)
newTodo.value = ''
}
}
</script>
스토어의 상태를 구조분해할당으로 가져오면 더 간결하게 사용할 수 있습니다:
<template>
<div>
<h3>현재 카운트: {{ count }}</h3>
<h3>이름: {{ name }}</h3>
<button @click="increment">증가</button>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
// 반응성을 유지하기 위해 storeToRefs 사용
// (일반 구조분해할당은 반응성을 잃음)
const { count, name } = storeToRefs(store)
// actions는 구조분해할당해도 됨 (메소드이므로)
const { increment } = store
</script>
Pinia는 스토어 상태를 쉽게 관리할 수 있는 기능을 제공합니다:
<template>
<div>
<button @click="resetStore">스토어 초기화</button>
<button @click="updateName">이름 변경</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter'
const store = useCounterStore()
function resetStore() {
// 스토어를 초기 상태로 리셋
store.$reset()
}
function updateName() {
// $patch로 여러 상태를 한 번에 변경
store.$patch({
name: 'Pinia와 함께하는 Vue',
count: 100
})
// 또는 함수 형태로 사용
store.$patch((state) => {
state.count = 0
state.todos.push({ id: 3, text: '함수형 패치 사용하기', done: false })
})
}
</script>
// stores/userStore.js
import { defineStore } from 'pinia'
import axios from 'axios'
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
currentUser: null,
loading: false,
error: null
}),
getters: {
getUserById: (state) => {
return (id) => state.users.find(user => user.id === id)
}
},
actions: {
async fetchUsers() {
this.loading = true
try {
const response = await axios.get('/api/users')
this.users = response.data
this.error = null
} catch (err) {
this.error = err.message
this.users = []
} finally {
this.loading = false
}
},
async createUser(userData) {
this.loading = true
try {
const response = await axios.post('/api/users', userData)
this.users.push(response.data)
return response.data
} catch (err) {
this.error = err.message
throw err
} finally {
this.loading = false
}
},
async loginUser(credentials) {
this.loading = true
try {
const response = await axios.post('/api/login', credentials)
this.currentUser = response.data
localStorage.setItem('token', response.data.token)
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`
return response.data
} catch (err) {
this.error = err.message
throw err
} finally {
this.loading = false
}
},
logoutUser() {
this.currentUser = null
localStorage.removeItem('token')
delete axios.defaults.headers.common['Authorization']
}
}
})
페이지를 새로고침해도 상태를 유지하기 위해 pinia-plugin-persistedstate를 사용할 수 있습니다:
npm install pinia-plugin-persistedstate
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
스토어에서 사용하기:
// stores/counter.js
export const useCounterStore = defineStore('counter', {
state: () => ({ /* ... */ }),
getters: { /* ... */ },
actions: { /* ... */ },
// 이 스토어는 로컬 스토리지에 상태를 저장함
persist: true
// 또는 더 세부적인 설정:
// persist: {
// storage: localStorage,
// paths: ['count', 'todos'], // 특정 상태만 저장
// }
})
이렇게 Pinia를 활용하면 Vue 애플리케이션에서 상태 관리를 효율적으로 할 수 있다. 컴포넌트 간 데이터 공유가 쉬워지고, 상태 변경 로직이 중앙화되어 유지보수가 편해진다! Spring Boot와 같은 백엔드와 연동할 때도 API 호출과 데이터 관리를 체계적으로 할 수 있어 풀스택 개발에도 도움이 될 것 같다!