[Vue] State Management

young-gue Park·2023년 11월 7일
0

Vue.js

목록 보기
8/10
post-thumbnail

⚡ State Management


📌 State Management (상태관리)

🔷 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. 여러 뷰가 동일한 상태에 종속되는 경우

  • 공유 상태를 공통 조상 컴포넌트로 끌어올린 다음 props로 전달하는 것
  • 하지만 계층 구조가 깊어질 경우 비효율적, 관리가 어려워 짐

2. 서로 다른 뷰의 기능이 동일한 상태를 변경시켜야 하는 경우

  • 발신(emit)된 이벤트를 통해 상태의 여러 복사본을 변경 및 동기화 하는 것
  • 마찬가지로 관리의 패턴이 깨지기 쉽고 유지 관리할 수 없는 코드가 됨

🤔 아니 이걸 어떻게 해결하는데요..?

💡 리액트에서는 문제 상황을 Prop Drilling이라 명하고 해결 방법으로 Context Api 등을 제공했었다.

Vue에서는

  1. 각 컴포넌트의 공유 상태를 추출하여, 전역에서 참조할 수 있는 저장소에서 관리

  2. 컴포넌트 트리는 하나의 큰 “뷰” 가 되고 컴포넌트는 트리 계층 구조에 관계없이 상태에 접근하거나 기능을 사용할 수 있다.

💡 Vue의 공식 상태 관리 라이브러리는 Pinia 이다.


📌 Pinia

💡 Vite 프로젝트 빌드 시 Pinia 라이브러리를 추가한다.

🔷 Pinia 구성 요소

  • Piniastore 라는 저장소를 가진다.
  • storestate, 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() === state
const count = ref(0)
  • store 인스턴스로 state에 접근하여 직접 읽고 쓸 수 있다.
  • 만약 store 에 state를 정의하지 않았다면 컴포넌트에서 새로 추가할 수 없다!

🍍 getters

  • 계산된 값
  • computed() === getters
const doubleCount = computed(() => count.value * 2)
  • store의 모든 getters를 state 처럼 직접 접근 할 수 있다.

🍍 actions

  • 메서드
  • function() === actions
const increment = function () {
	count.value++
}
  • store의 모든 actions를 직접 접근 및 호출 할 수 있다.
  • getters 와 달리 state 조작, 비동기, API 호출이나 다른 로직을 진행할 수 있다.

🖥 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

  • 어플리케이션의 상태 관리에 필요한 추가 기능을 제공하거나 확장하는 도구나 모듈
  • 어플리케이션의 상태 관리를 더욱 간편하고 유연하게 만들어주며 패키지 매니저로
    설치 이후 별도 설정을 통해 추가됨

🍍 Pinia 실습

원래 프론트엔드 라이브러리와 프레임워크 실습은 Todo List가 국룰이다.

🔷 Pinia 를 활용한 Todo 프로젝트 구현

  • Todo CRUD
  • Todo 개수 계산
    • 전체 Todo
    • 완료된 Todo
    • 미완료된 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에 속아 방심하지 말아야겠다.

profile
Hodie mihi, Cras tibi

0개의 댓글

관련 채용 정보