Vue 컴포넌트는 이미 반응형 상태를 관리하고 있음
⇒ 상태 === 데이터
상태 (State)
뷰 (View)
기능 (Actions)
⇒ “단방향 데이터 흐름”의 간단한 표현
<template>
<!-- 뷰 -->
<div>
</div>
</template>
<script setup>
// 상태
const count = ref(0)
// 기능
const increment = function(){
count.value++
}
</script>
<style scoped>
</style>
“여러 컴포넌트가 상태를 공유할 때”
각 컴포넌트의 공유 상태를 추출하여, 전역에서 참조할 수 있는 저장소에서 관리
컴포넌트 트리는 하나의 큰 View가 되고 모든 컴포넌트는 트리 계층 구조에 관계 없이 상태에 접근하거나 기능을 사용할 수 있음
→ Vue의 공식 상태 관리 라이브러리 === “Pinia”
Vue 공식 상태 관리 라이브러리
stores 폴더 신규 생성storestategettersactionspluginstore’모든 컴포넌트가 공유하는 상태, 기능 등이 작성됨
⇒ defineStore()의 반환 값의 이름은 use와 store를 사용하는 것을 권장
⇒ defineStore()의 첫번째 인자는 애플리케이션 전체에 설쳐 사용하는 store의 고유 ID
state’ref() === stategetters’computed() === gettersactions’function() === actionspinia의 상태들을 사용하려면 반드시 반환해야 함
※ store에서는 공유 하지 않는 private한 상태 속성을 가지지 않음
plugin’Pinia는 store라는 저장소를 가짐
store는 state, getters, actions으로 이루어지며 각각 ref(), computed(), function()과 동일함
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(()=>count.value *2)
const increment = function (todoText) {
count.value++
}
return { count, doubleTodo, increment }
}, { persist: true })
```
각 컴포넌트 깊이에 관계 없이 store 인스턴스로 state에 접근하여 직접 일고 쓸 수 있음
만약 store에 state를 정의하지 않았다면 컴포넌트에서 새로 추가할 수 없음
<template>
<div>
<p>state : {{ store.count }}</p>
</div>
</template>
<script setup>
import TodoForm from '@/components/TodoForm.vue'
import TodoList from '@/components/TodoList.vue'
import { useCounterStore } from './stores/counter'
const store = useCounterStore()
// state 참조 및 변경
const newNumber = store.count + 1
</script>
<style scoped>
</style>
store의 모든 getters 또한 state처럼 직접 접근 할 수 있음
<template>
<div>
<p>getters : {{ store.count }}</p>
</div>
</template>
<script setup>
import TodoForm from '@/components/TodoForm.vue'
import TodoList from '@/components/TodoList.vue'
import { useCounterStore } from './stores/counter'
const store = useCounterStore()
// state 참조 및 변경
console.log(store.count)
const newNumber = store.count + 1
</script>
<style scoped>
</style>
store의 모든 actions 또한 직접 접근 및 호출 할 수 있음
getters와 달리 state 조작, 비동기, API 호출이나 다른 로직을 진행할 수 있음
<template>
<div>
<button @click="store.increment()">+++</button>
</div>
</template>
<script setup>
import TodoForm from '@/components/TodoForm.vue'
import TodoList from '@/components/TodoList.vue'
import { useCounterStore } from './stores/counter'
const store = useCounterStore()
console.log(store.count)
store.increment()
</script>
<style scoped>
</style>
초기 생성된 컴포넌트 모두 삭제 (App.vue 제외)
src/assets 내부 파일 모두 삭제
main.js 해당 코드 삭제
TodoListItem 컴포넌트 작성
TodoList 컴포넌트 작성
TodoListItem 컴포넌트 등록
TodoForm 컴포넌트 작성
App 컴포넌트에 TodoList, TodooForm 컴포넌트 등록
<template>
<div>
<h1>Todo Project</h1>
<TodoList/>
<TodoForm/>
</div>
</template>
컴포넌트 구성 확인
store에 임시 todos 목록 state를 정의
//stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
let id = 0
const todos = ref([
{id : id++ ,text : 'todo1', isDone:false},
{id : id++ ,text : 'todo2', isDone:false},
])
return { todos}
})
store의 todos state를 참조
하위 컴포넌트인 Todo ListItem을 반복 하면서 개별 todo를 props로 전달
```jsx
//TodoList.vue
<template>
<div>
<TodoListItem v-for="todo in store.todos" :key="todo.id" :todo="todo"/>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
import TodoListItem from './TodoListItem.vue';
const store = useCounterStore()
</script>
<style scoped>
</style>
```
props 정의 후 데이터 출력 확인
//TodoListItem.vue
<template>
<div>
<li>{{ todo.text }}</li>
</div>
</template>
<script setup>
defineProps({
todo : Object
})
</script>
<style scoped>
</style>
todos 목록에 todo를 생성 및 추가하는 addTodo 액션 정의
const addTodo = (todoText)=>{
todos.value.push({
id:id++, text:todoText, isDone:false
})
}
TodoForm에서 실시간으로 입력되는 사용자 데이터를 양방향 바인딩하여 반응형 변수로 할당
<template>
<div>
<h2>Todo 추가하기</h2>
<form @submit.prevent="addTodo(todoText)">
<input type="text" v-model="todoText">
<input type="submit">
</form>
</div>
</template>
<script setup>
import {ref} from 'vue'
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore()
const todoText = ref('')
</script>
<style scoped>
</style>
submit 이벤트가 발생 했을 때 사용자 입력 텍스트를 인자로 전달하여 store에 정의한 addTodo 액션 메서드를 호출
<template>
<div>
<h2>Todo 추가하기</h2>
<form @submit.prevent="store.addTodo(todoText)">
<input type="text" v-model="todoText">
<input type="submit">
</form>
</div>
</template>
<script setup>
import {ref} from 'vue'
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore()
const todoText = ref('')
</script>
<style scoped>
</style>
form 요소를 선택하여 todo 입력 후 input 데이터를 초기화 할 수 있도록 처리
<template>
<div>
<h2>Todo 추가하기</h2>
<form @submit.prevent="addTodo(todoText)" ref="formElem">
<input type="text" v-model="todoText">
<input type="submit">
</form>
</div>
</template>
<script setup>
import {ref} from 'vue'
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore()
const todoText = ref('')
const formElem = ref(null)
const addTodo = (todoText)=>{
store.addTodo(todoText)
formElem.value.reset()
}
</script>
<style scoped>
</style>
todos 목록에서 특정 todo를 삭제한 deleteTodo 액션 정의
const deleteTodo = ()=>{
console.log('delete')
}
각 todo에 삭제 버튼을 작성
버튼을 클릭하면 선택된 todo의 id를 인자로 전달해 deleteTodo 메서드 호출
```jsx
<template>
<div>
<li>{{ todo.text }}
<button @click="store.delteTodo(todo.id)">Delete</button>
</li>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
defineProps({
todo : Object
})
const store = useCounterStore()
</script>
```
전달받은 todo의 id 값을 활용해 선택된 todo의 인덱스를 구함
특정 인덱스 todo를 삭제 후 todos 배열을 재설정
```jsx
const deleteTodo = (todoId)=>{
const index = todos.value.findIndex((todo)=> todo.id === todoId)
todos.value.splice(index,1)
}
```
결과 확인

“각 todo 상태의 isDone 속성을 변경하여 todo의 완료 유무 처리하기”
완료된 todo에는 취소선 스타일 적용하기
todos 목록에서 특정 todo의 isDone 속성을 변경하는 updateTodo 액션 정의
```jsx
const updateTodo = (todoId)=>{
}
```
todo 내용을 클릭하면 선택된 todo의 id를 인자로 전달해 updateTodo 메서드 호출
<li @click="store.updateTodo(todo.id)">
{{ todo.text }}
<button @click="store.deleteTodo(todo.id)">Delete</button>
</li>
전달받은 todo의 id 값을 활용해 선택된 todo와 동일 todo를 목록에서 검색
일치하는 todo 데이터의 isDone속성 값을 반대로 재할당 후 새로운 todo 목록 반환
```jsx
const updateTodo = (todoId)=>{
todos.value = todos.value.map((todo)=>{
if(todo.id === todoId){
todo.isDone = !todo.isDone
}
return todo
})
}
```
todo 객체의 isDone 속성 값에 따라 스타일 바인딩 적용하기
<span @click="store.updateTodo(todo.id)" :class="{'is-done':todo.isDone}">
{{ todo.text }}
</span>
----
<style scoped>
.is-done{
text-decoration: line-through;
}
</style>
todos 배열의 길이 값을 반환하는 함수 doneTodosCount 작성 (getters)App 컴포넌트에서 doneTodosCount getter를 참조const doneTodosCount = computed(()=>{
let count = 0
todos.value.forEach(todo => {
if (todo.isDone){
count ++
}
});
return count
})
----
<template>
<div>
<h1>Todo Project</h1>
완료된 todo 개수 : {{ store.doneTodosCount }}
<hr>
</div>
</template>
브라우저 내에 key-value 쌍을 저장하는 웹 스토리지 객체
state)를 브라우저의 local storage나 session storage에 영구적으로 저장하고 복원하는 기능을 제공설치 및 등록
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// app.use(createPinia())
app.use(pinia)
app.mount('#app')
defineStore()의 3번째 인자로 관련 객체 추가export const useCounterStore = defineStore('counter', () => {
...
return { todos, addTodo, deleteTodo, updateTodo, doneTodoCount }
}, { persist: true })
적용 결과 (개발자도구 → Application → Local Storage)
Local Storage에 저장되는 todos state 확인state에 넣어야 하는 것은 아님pass props, emit event를 함께 사용하여 애플리케이션을 구성 해야 함Pinia는 공유된 상태를 관리하는 데 유용하지만, 구조적인 개념에 대한 이해와 시작하는 비용이 큼
애플리케이션이 단순하다면 Pinia가 없는 것이 더 효율적일 수 있음
그러나 중대형 규모의 SPA를 구축하는 경우 Pinia는 자연스럽게 선택할 수 있는 단계가 오게 됨
⇒ 결과적으로 적절한 상황에서 활용 했을 때 Pinia 효용을 극대화 할 수 있음