05.20 학습

한강섭·2025년 5월 20일
0

학습 & 숙제

목록 보기
91/103
post-thumbnail

Vue - State Management


🔄 State Management

Vue 컴포넌트는 이미 반응형 상태를 관리하고 있다.

상태 === 데이터

컴포넌트 구조의 단순화

상태 (state) , 뷰 (View) , 기능 (Actions)
-> 단방향 데이터 흐름의 간단한 표현

상태 관리의 단순성이 무너지는 시점

여러 컴포넌트가 상태를 공유할 때
1. 여러 뷰가 동일한 상태에 종속되는 경우
2. 서로 다른 뷰의 기능이 동일한 상태(데이터)를 변경시켜야 하는 경우

계층 구조가 깊어질 경우 비효율적, 관리가 어려워진다!

해결책

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

Vue의 공식 상태 관리 라이브러리 === "Pinia"


🍍 Pinia

store, state, getters, actions, plugin 으로 구성되어 있다.

store

중앙 저장소, 모든 컴포넌트가 공유하는 상태, 기능 등이 작성됨

state

반응형 상태(데이터), ref() === state

getters

계산된 값, conputed() === getters

actions

메서드, function() === actions

plugin

애플리케이션의 상태 관리에 필요한 추가 기능을 제공하거나 확장하는 도구


💻 실습


1️⃣ Pinia 설치하기

먼저 프로젝트에 Pinia를 설치:

npm install pinia

2️⃣ 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')

3️⃣ 스토어 정의하기

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
      }
    }
  }
})

4️⃣ 컴포지션 API와 함께 사용하기

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>

5️⃣ Todo 관리 컴포넌트 만들기

<!-- 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>

6️⃣ 스토어 상태 구조분해할당으로 사용하기

스토어의 상태를 구조분해할당으로 가져오면 더 간결하게 사용할 수 있습니다:

<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>

7️⃣ 스토어 리셋 및 상태 변경하기

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>

8️⃣ Pinia와 Spring Boot 백엔드 연동하기

// 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']
    }
  }
})

9️⃣ 영구 상태 저장하기 (persistedstate)

페이지를 새로고침해도 상태를 유지하기 위해 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 호출과 데이터 관리를 체계적으로 할 수 있어 풀스택 개발에도 도움이 될 것 같다!

profile
기록하고 공유하는 개발자

0개의 댓글