🔷 Vue 컴포넌트는 이미 반응형 상태를 관리하고 있다.
💡 상태 === 데이터
🔷 컴포넌트 구조의 단순화
<template>
<!-- 뷰(view) -->
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue’
// 상태(State)
const count = ref(0)
// 기능(Actions)
const increment = function () {
count.value++
}
</script>
🔷 상태 (State)
🔷 뷰 (View)
🔷 기능 (Action)
💡
단방향 데이터 흐름
의 간단한 표현이다.
🔷 상태 관리의 단순성이 무너지는 시점
1. 여러 뷰가 동일한 상태에 종속되는 경우
2. 서로 다른 뷰의 기능이 동일한 상태를 변경시켜야 하는 경우
🤔 아니 이걸 어떻게 해결하는데요..?
💡 리액트에서는 문제 상황을 Prop Drilling이라 명하고 해결 방법으로 Context Api 등을 제공했었다.
Vue에서는
각 컴포넌트의 공유 상태를 추출하여, 전역에서 참조할 수 있는 저장소에서 관리
컴포넌트 트리는 하나의 큰 “뷰” 가 되고 컴포넌트는 트리 계층 구조에 관계없이 상태에 접근하거나 기능을 사용할 수 있다.
💡 Vue의 공식 상태 관리 라이브러리는
Pinia
이다.
💡 Vite 프로젝트 빌드 시 Pinia 라이브러리를 추가한다.
🔷 Pinia 구성 요소
Pinia
는 store
라는 저장소를 가진다.store
는 state
, getters
, actions
으로 이루어지며 각각 ref()
, computed()
, function()
과 동일하다.🖥 프로젝트를 빌드하면 생성되는 counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
//state
const count = ref(0)
//getters
const doubleCount = computed(() => count.value * 2)
//actions
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
🍍 store
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = function () {
count.value++
}
return { count, doubleCount, increment }
})
🍍 state
ref()
=== stateconst count = ref(0)
🍍 getters
computed()
=== gettersconst doubleCount = computed(() => count.value * 2)
🍍 actions
function()
=== actionsconst increment = function () {
count.value++
}
🖥 counter.js 사용하기
<template>
<div>
<h2>StoreTest</h2>
<p>state: {{ store.count }}</p>
<p>getters: {{ store.doubleCount }}</p>
<button @click="store.increment()">증가</button>
<button @click="increment2()">증가2</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore();
//되긴 하는데 추천하지 않음
const increment2 = () => {
store.increment();
}
</script>
<style scoped>
</style>
작동 확인
🍍 plugin
원래 프론트엔드 라이브러리와 프레임워크 실습은 Todo List가 국룰이다.
🔷 Pinia 를 활용한 Todo 프로젝트 구현
🍍 TodoList.vue
<template>
<div>
<h4>TodoList</h4>
<!--store 의 todos 상태를 참조-->
<!--하위 컴포넌트인 TodoListItem 을 반복 하면서 개별 todo를 props로 전달-->
<TodoListItem v-for="todo in store.todos" :key="todo.id" :todo="todo" />
</div>
</template>
<script setup>
import TodoListItem from './TodoListItem.vue';
import { useTodosStore } from '@/stores/todos';
const store = useTodosStore();
</script>
<style scoped>
</style>
🍍 TodoForm.vue
<template>
<div>
<h4>TodoForm</h4>
<!--실시간으로 입력되는 사용자 데이터를 양방향 바인딩하여 반응형 변수로 할당-->
<input type="text" v-model.trim="todoText" @keyup.enter="createTodo"/>
<button @click="createTodo">등록</button>
</div>
</template>
<script setup>
import { useTodosStore } from '@/stores/todos';
import { ref } from 'vue';
const store = useTodosStore();
const todoText = ref('');
const createTodo = () => {
//가벼운 유효성 검사
if (todoText.value) {
store.addTodo(todoText.value);
todoText.value = "";
}
else
alert('내용을 입력해주세요!');
}
</script>
<style scoped>
</style>
🍍 TodoListItem.vue
<template>
<div>
<!--todo 내용을 클릭하면 선택된 todo의 id를 인자로 전달해 updateTodo 메서드를 호출-->
<!--todo 객체의 isDone 속성 값에 따라 스타일 바인딩 적용하기-->
<span class="click-cursor" :class="{'is-done': todo.isDone}" @click="store.updateTodo(todo.id)">{{ todo.text }}</span>
<!--버튼을 클릭하면 선택된 todo의 id를 인자로 전달해 deleteTodo 메서드 호출-->
<button @click="store.deleteTodo(todo.id)">X</button>
</div>
</template>
<script setup>
import { useTodosStore } from '@/stores/todos';
const store = useTodosStore();
//props 정의
defineProps({
todo: Object,
})
</script>
<style scoped>
.is-done {
text-decoration: line-through;
}
.click-cursor {
cursor: pointer;
}
</style>
🍍 stores/todos.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useTodosStore = defineStore('todos', () => {
let id = 0;
//state
//임시 todos 목록 상태를 정의
const todos = ref([
{
id: id++,
text: '야무지게 수업듣기',
isDone: false,
},
{
id: id++,
text: '기깔나게 점심먹기',
isDone: false,
},
{
id: id++,
text: '무지성으로 게임하기',
isDone: false,
},
{
id: id++,
text: '정신차리고 복습하기',
isDone: false,
},
])
//actions
//todos 목록에 todo를 생성 및 추가하는 addTodo 액션
const addTodo = (todoText) => {
todos.value.push({
id: id++,
text: todoText,
isDone:false,
})
}
//todos 목록에서 특정 todo를 삭제하는 deleteTodo 액션
//전달 받은 todo의 id 값을 활용해 선택된 todo의 인덱스를 구함
//특정 인덱스 todo를 삭제 후 todos 배열을 재설정
const deleteTodo = (todoId) => {
//index를 비교하여 일치하는 대상의 인덱스를 반환하여 저장
const index = todos.value.findIndex((todo) => todo.id === todoId)
todos.value.splice(index, 1);
}
//todos 목록에서 특정 todo의 isDone 속성을 변경하는 updateTodo 액션
//각 todo 상태의 isDone 속성을 변경하여 todo의 완료 유무 처리하기
//전달 받은 todo의 id 값을 활용해 선택된 todo 와 동일 todo를 목록에서 검색
//일치하는 todo 데이터의 isDone 속성 값을 반대로 재할당 후 새로운 todo 목록 반환
const updateTodo = (todoId) => {
todos.value = todos.value.map((todo) => {
if (todo.id === todoId) {
todo.isDone = !todo.isDone;
}
return todo;
})
}
//getters
//todos 배열의 길이 값을 반환하는 함수 doneTodosCount
const doneTodoCount = computed(() => {
//참인 밸류만 모아서 배열로 반환
return todos.value.filter((todo) => todo.isDone).length;
})
return { todos, addTodo, deleteTodo, updateTodo, doneTodoCount };
}, { persist: true }) //{persist} 관련 객체 추가를 통해 pinia-plugin-persistedstate 설정
🍍 App.vue
<template>
<div>
<h2>Todo PJT</h2>
<!--doneTodosCount getter를 참조-->
<p>완료한 Todo 개수: {{ store.doneTodoCount }}</p>
<TodoForm />
<TodoList />
</div>
</template>
<script setup>
import TodoForm from './components/TodoForm.vue';
import TodoList from './components/TodoList.vue';
import { useTodosStore } from '@/stores/todos';
const store = useTodosStore();
</script>
<style scoped>
</style>
🍍 main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
//pinia-plugin-persistedstate 사용을 위함
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
🔷 LocalStorage를 사용하여 삭제했거나, 완료 표시를 했거나, 새로 추가한 todo를 모두 기억했다가 브라우저가 다시 실행되었을 때 꺼내온다.
💡
LocalStorage
브라우저 내에 key-value 쌍을 저장하는 웹 스토리지 객체, 이전에 다룬 적이 있다.
💡
pinia-plugin-persistedstate
Pinia 의 플로그인(plugin) 중 하나로써, 웹 어플리케이션의 상태 (state)를 브라우저의 local storage나 session storage에 영구적으로 저장하고 복원하는 기능을 제공한다. 링크
🍍 사용 해보자
로컬 저장소에서 데이터를 꺼내왔다. 이전에 0번 id의 todo를 삭제하고 4번 id의 todo를 추가한 것을 기억하고 있었다.
todo를 클릭하면 완료했음이 표시된다. 이에 따라 완료 개수 또한 바뀐다.
추가 역시 깔끔하고
삭제도 깔끔하다.
브라우저를 새로고침해도 변경 사항은 유지된다.
내일은 Ajax인데, 리액트를 배울 때 벽을 느끼게 했던 파트였다.
익숙한 Todo에 속아 방심하지 말아야겠다.