Vue 3.0/07.pinia

[vue3-pinia] 02. todos 관리

  • -

이번 포스트에서는 pinia를 통해 일정 관리 앱을 만들어 보자. 미리 이야기 하지만 사실 이 예는 pinia의 사용법에 대해 이야기 하기 위해 작성하는 것이지 이런 경우에 pinia가 유리하다는 이야기는 절.대. 아니라는 점을 미리 이야기 해둔다.

 

todo 개요

아래와 같이 구성된 Todo 관리 애플리케이션을 작성한다고 생각해보자.

Todo 관리 애플리케이션! 할일의 목록은 어디에서 관리할 것인가?

TodoView의 자식 컴포넌트로 TodoList에서 목록을 보여주고 수정, 삭제 작업을 처리하며 TodoControl에서 추가,필터링 등의 작업을 처리한다. 이 경우 Todo 목록을 어디에 관리하는 것이 좋을까? 

  • 목록이니까 TodoList에서 관리한다면 할일 추가를 위해서 TodoControl에서 emit 후 TodoView에서 props로 TodoList에 내려주는 형식이 가능하다.
  • TodoControl에서 관리한다면 TodoList에서 보여줄 정보를 emit으로 TodoView에게 전달하고 다시 props로 내려준다.
  • TodoView에서 관리한다면 모든 자식들은 emit으로 부모에게 하려는 일을 통보하고 TodoView는 props로 내려준다.

즉 안되는 것은 아니다. 하지만 이런 과정에서 깊이가 깊어진다면 복잡도가 지나치게 상승할 것이고 어딘가 중앙의 저장소가 있다면 아주 유리할 것 같다. 이처럼 컴포넌트간의 공유가 필요한 자료가 있는 경우 사용하는 중앙 저장소 역할을 하는 것이 pinia이다.

그럼 pinia를 구성하는 3가지 요소 state, getters, actions를 하나씩 작성해보자.

 

State

 

state의 정의

state는 store의 가장 핵심적인 부분이다. state에는 컴포넌트간 공유할 데이터를 작성한다. 이때 컴포넌트에서와 마찬가지로 화면에서의 반응성을 원한다면 ref로 감싸서 처리하고 그냥 고정 값을 사용한다면 감쌀 필요가 없다.

다음은 할일 관리를 위해서 필요한 state들을 정의한 예이다.

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todo', () => {
  let nextId = 1 // 굳이 공개할 필요 없는 내부 변수
  const todos = ref([]) // 이런걸 store에서 관리한다는 건 사실 non sense
  const keys = ['all', 'ing', 'done'] // 반응성이 필요 없는 고정 데이터
  const key = ref('all')

  // action에 해당하는 insertTodo와 deleteTodo를 작성해보자.
  const insertTodo = null, deleteTodo = null
  
  // getters에 해당하는 needTodoCnt, filteredTodo를 작성해보자.
  const needTodoCnt = null, filteredTodo = null
  
  return { todos, keys, key, insertTodo, deleteTodo, needTodoCnt, filteredTodo }
})

 

state 활용

이 state를 component에서 사용해 보자. store.js에서 export한 useTodoStore는 함수 형태로 reactive 한 store 객체를 리턴한다.  

import { useTodoStore } from '@/stores/todo.js'
const todoStore = useTodoStore()
console.log(`[TodoList.vue] todoStore:`, todoStore)
console.log(`[TodoList.vue] todos:`, todoStore.todos)
console.log(`[TodoList.vue] todos:`, todoStore.keys)
console.log(`[TodoList.vue] todos:`, todoStore.key)

store는 Proxy 형태로 얻어지므로 그냥 편하게 쓰면 된다.

 

action

 

state 변경

component에서 state를 변경하는 것도 기본적으로는 자유롭게 허용하는 편이다. 특히 반응형 값은 화면에서 v-model로 등록해서 사용 가능하다.

todoStore.todos.push({ id: new Date().getTime(), text: newTodo.value, isDone: false })

<select v-model="todoStore.key">
  <option :value="key" v-for="key in todoStore.keys" :key="key">{{ key }}</option>
</select>

 

또는 store의 내장 함수인 $patch()를 이용할 수 있는데 $patch에는 간단히 객체 형으로 값을 전달할 수도 있고 복잡한 처리가 필요한 경우 함수 형태로 전달할 수도 있다.

// 간단한 것은 객체 형태로 처리
todoStore.$patch({
  key: 'some'
})

// 복잡한 경우는 state를 파라미터로 받는 함수를 전달 받아서 처리
todoStore.$patch((state) => {
  state.todos.push({ id: new Date().getTime(), text: newTodo.value, isDone: false })
})

하지만 이렇게 개별 컴포넌트에서 전역 데이터를 제약 없이 직접 수정하기 보다는 뒤에 action을 이용해서 일관성 있게 수정하는 것이 안정적이다.

 

action

action이란 store에서 state를 변경시키는 역할을 수행하는 함수로 composition 방식에서는 일반 function으로 작성하면 된다. 여기서는 동기 작업은 물로 비동기도 문제없이 작성할 수 있다.

다음은 전달받은 todo를 저장하거나 전달받은 id에 해당하는 todo를 삭제하는 action이다.

const insertTodo = (text) => {
  todos.value.push({ id: nextId++, text, isDone: false })
}

const deleteTodo = (id) => {
  for (let i = 0; i < todos.value.length; i++) {
    if (todos.value[i].id === id) {
      todos.value.splice(i, 1)
      break
    }
  }
}
기존에 vuex를 쓸 때에는 특정 메서드를 호출해야 하고, 동기와 비동기를 나눠서 했어야 했기 때문에 복잡했는데 "이제 그냥 함수다." 끝!

 

getters

 

getters

getters는 vue component에서 사용하는 computed와 목적이 동일하다. computed의 목적은? 기존 데이터를 건드리지 않은 상태에서 캐싱과 필터링이라고 볼 수 있다. 

그래서 composition 방식에서 getters를 만들 때는 readonly 성격을 갖는 computed를 사용한다.

const needTodoCnt = computed(() => {
  console.log(`[todo.js] needTodoCnt호출됨:`)
  return todos.value.filter((todo) => !todo.isDone).length
})

 

getters를 이용한 filtering

getters를 이용한 filtering은 매우 유용한 기능이다. 예를 들어 진행중인 todo만 보고싶거나 완료된 todo를 보고 싶을 때 getter를 사용하면 유용하다.

const filteredTodo = computed(() => {
  if (key.value == 'all') {
    return todos.value
  } else {
    const done = key.value === 'ing' ? false : true
    const result = todos.value.filter((todo) => todo.isDone === done)
    return result
  }
})

 

일반적으로 computed의 callback 함수에는 파라미터를 사용하지 않는다. 만약 필터링을 위해서는 getters에 파라미터를 전달하고 싶은 경우 computed에서 단순 값이 아닌 파라미터를 받는 함수를 반환하는 형태로 작성해주면 된다.

const filteredTodoWithParam = computed(() => {
  return (param)=> "do something"
})

이제 반환된 getters를 호출할 때 파라미터를 넘겨줄 수 있게 되었다.

<tr v-for="(todo, id) in filteredTodosWithParam(param)" :key="id">
    . . .
</tr>

 

다른 getters 사용

composition 형태를 사용하면 다른 getters를 사용하는 것도 매우 쉽다. 그냥 부르면 되니까? ㅎ

심지어 다른 store에서 작성된 getters를 사용하는 것도 물 흐르는것 처럼 자유롭다. 물론 다른 store의 state 도 가져올 수 있다. 이때 주의할 점은 store는 외부에 reactive 객체로 노출된다는 점을 잊지 말자.  언제나 반응성에 대한 주의가 필요하다. 외우지 말고 그냥 출력해보자. 

// counter.js 내용 일부  
const count = ref(0)
const user = reactive({"name":"hong"}); 
const doubleCount = computed(() => count.value * 2)
  
// 다른 store를 사용하고 싶다면?
import {useCounterStore} from '@/stores/counter.js'

const fromOther = computed(()=> {
    const counterStore = useCounterStore();                        // reactive
    console.log("counterstore count(ref): ",counterStore.count)    // 단순 값
    console.log("counterstore user(reactive): ",counterStore.user) // proxy
    return counterStore.doubleCount});

 

 

화면에서의 활용

다음은 store를 화면단에서 사용한 예이다.

<template>
  <div class="left">
    <table>
      <tr><th>no</th> <th>text</th> <th>done</th> <th>삭제</th></tr>
      <tr v-for="(todo, id) in todoStore.filteredTodo" :key="id">
        <td>{{ todo.id }}</td>
        <td>{{ todo.text }}</td>
        <td><input type="checkbox" v-model="todo.isDone" /></td>
        <td><button @click="todoStore.deleteTodo(todo.id)">삭제</button></td>
      </tr>
    </table>
  </div>
</template>
<template>
  <div class="right">
    <label>새 할일: <input v-model="newTodo" /></label>
    <button @click="todoStore.insertTodo(newTodo)">등록</button> <hr />
    <label>처리할 일 개수: {{ todoStore.needTodoCnt }}</label> <hr />
    <label>상태별 필터링:</label>
    <select v-model="todoStore.key">
      <option :value="key" v-for="key in todoStore.keys" :key="key">{{ key }}</option>
    </select>
  </div>
</template>

 

'Vue 3.0 > 07.pinia' 카테고리의 다른 글

[vue3-pinia] 04. pinia-plugin-persistedstate  (2) 2023.10.15
[vue3-pinia] 03. 기타  (0) 2023.10.15
[vue3-pinia] 01. Pinia 개요  (0) 2023.10.14
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.