Home Pinia 공식문서 보고 적용하기
Post
Cancel

Pinia 공식문서 보고 적용하기


What is Pinia?

참고: 공식문서
https://pinia.vuejs.org/introduction.html#introduction


Introduction

Pinia는 2019년 11월경에 Composition API를 사용하여 Vue의 Store가 어떻게 디자인 될 수 있는 지를 재설계하는 실험으로 시작되었습니다. 이후 초기 원칙은 그대로 유지되었지만, Pinia는 Vue 2와 Vue 3 모두에서 작동하며 꼭 Composition API를 사용할 필요가 없습니다.


피니아 제공 기능

💡 Note:
개발자 도구에서 확인할 수 있다.
타입스크립트를 사용할 수 있다.

  • Devtools support
    • A timeline to track actions, mutations
    • Stores appear in components where they are used
    • Time travel and easier debugging
  • Hot module replacement
    • Modify your stores without reloading your page
    • Keep any existing state while developing
  • Plugins: extend Pinia features with plugins
  • Proper TypeScript support or autocompletion for JS users
  • Server Side Rendering Support


Why Pinia

Pinia는 파인애플의 스페인어인 piña 와 비슷한 이름을 가진다. 파인애플은 실제로 여러 개의 개별 꽃이 결합하여 만드는 과일이다. Pinia가 제공하는 Store와 유사한 점으로 각각 개별적으로 태어나지만(생성되지만) 결국은 모두 연결되어 있다는 점이 비슷하다.


Comparison with Vuex 3.x/4.x

💡 Note:
더 이상 mutations 은 안쓴다.
타입스크립트를 쓰기위해 복잡한 wrapper를 생성할 필요가 없다.

  • No more magic strings to inject, import the functions, call them, enjoy autocompletion!
  • No need to dynamically add stores, they are all dynamic by default and you won’t even notice. Note you can still manually use a store to register it whenever you want but because it is automatic you don’t need to worry about it.
  • No more nested structuring of modules. You can still nest stores implicitly by importing and using a store inside another but Pinia offers a flat structuring by design while still enabling ways of cross composition among stores. You can even have circular dependencies of stores.
  • No namespaced modules. Given the flat architecture of stores, “namespacing” stores is inherent to how they are defined and you could say all stores are namespaced.



Defining a Store

Option Stores

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineStore } from pinia;
export const useProduectStore = defineStore( store id, {
    state: (){
        return {
        
        }
    },
    // ✅ could also be defined as
    // state: () => ({ count: 0 })
    
    // getters
	
    // actions

}


Setup Stores

Vue Composition API’s setup function 과 비슷한 문법으로 store 를 정의할 수 있다.

1
2
3
4
5
6
7
8
9
10
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

In Setup Stores:

  • ref()state
  • computed()getters
  • function()actions

Setup stores는 Option Stores 보다 유연하다. composable로 사용할 수 있고 watch를 사용할 수 있다. 그러나 composables를 사용하면 SSR에서 더 복잡해진다는 것을 주의하자.


What syntax should I pick?

편해보이는 것을 선택하자. 만약 잘 모르겠다면 Options Stores를 먼저 시도해보자.


Using the store

store를 생성 및 작성했다고 store를 쓸 수 있는 것이 아니다. 실제로 사용하기 위해서는 <script setup> 컴포넌트에 적용을 해주어야 한다.

스토어는 reactive로 감싸진 객체이므로, getters에 .value를 쓸 필요가 없지만 구조분해(destructure)는 불가능하다. 하지만 storeToRefs()를 사용하면 구조분해방식으로 reactivity를 유지한 채 properties를 꺼낼 수 있다. 다만 이것은 actions을 사용하지 않고 오직 state만 사용할 때 유용하다. 추가로 action은 store 자체에 바인딩되어 있기 때문에 직접 store에서 action을 구조 분해할 수 있다.

단, storeToRef()를 이용해 구조 분해한 state 값은 .value를 사용해서 value값을 변경할 수 있다. 마이그레이션을 하면서, storeToRef()는 value를 사용해야하는 점이 번거로워 구조 분해 없이 사용했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
import { useCounterStore } from '@/stores/counter'

// access the `store` variable anywhere in the component ✨
const store = useCounterStore()

// ❌ state 불러오기 적절하지 않은 방법-구조분해
// This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = store 
name // will always be "Eduardo" 
doubleCount // will always be 0

// ✅ state 불러오기 적절한 방법
// this one will be reactive
// 💡 but you could also just use `store.doubleCount` directly
const doubleValue = computed(() => store.doubleCount)
또는
import { storeToRefs } from 'pinia'
const { name, doubleCount } = storeToRefs(store)

// the increment action can just be destructured
const { increment } = store
</script>

테스트를 해봤는데 왜 storeToRef()가 actions 옵션이 있을 때 보다 state만 있을 때 유용한지 알겠다. 나는 보통 actions에도 state와 같은 이름으로 함수명을 짓는데 그러면 같은 이름이 존재하게 되면서 storeToRef()로 검색이 안되어 undefined가 뜰 수 있다.

+추가로
vuex에서는 dispatch함수 또는 state 경로를 통해 actions인지 state인지 구분할 수 있어서 actions의 함수와 state의 값의 이름을 같게 했지만, 피니아에서 actions와 state의 이름을 같게 지으면 혼동의 여지가 있어서 이름을 다르게 하는 것을 추천한다.

1
2
3
4
5
6
7
8
9
// state와 actions의 이름이 같은 경우 
// ❌ 괄호를 이용해 구분하기 때문에 잘못 쓴 경우에도 파악하지 못하고 지나칠 경우가 있다.
mainStore.scriptList;
mainStore.scriptList();

// state와 actions의 이름이 다른 경우 
// ✅ 구분이 용이해 헷갈리지 않음
mainStore.scriptList;
mainStore.editScriptList();


Using a store outside of a component

main.js 에서 피니아 파일을 불러오는 방법:
SSR(Server Side Rendering)이 아니라면 import한 store 파일을 app.use(pinia) 선언 이후에 호출하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia';
import { createApp } from 'vue'
import App from './App.vue'

// ❌  fails because it's called before the pinia is created
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ works because the pinia instance is now active
const userStore = useUserStore()


Usage Outside Components

컴포넌트 파일이 아닌 다른 확장자 파일에 store import 하는 방법:
파일을 import 한 뒤, 함수 안 에서 store를 사용해야 한다.

1
2
3
4
5
6
7
8
9
// Pinia
import { useAuthUserStore } from '@/stores/auth-user'

router.beforeEach((to, from, next) => {
  // ⚠️ Must be used within the function! ⚠️
  const authUserStore = useAuthUserStore()
  if (authUserStore.loggedIn) next()
  else next('/login')
})


Migrating from Vuex ≤4

vuex는 여러개의 modules가 있는 하나의 store이다.

modules는 namespaced 일 수 있고 서로 nested 되었을 수 도 있다.

Pinia를 쓰기 위해 가장 쉬운 방법은 각각의 모듈을 store로 만드는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Vuex example (assuming namespaced modules)
src
└── store
    ├── index.js           # Initializes Vuex, imports modules
    └── modules
        ├── module1.js     # 'module1' namespace
        └── nested
            ├── index.js   # 'nested' namespace, imports module2 & module3
            ├── module2.js # 'nested/module2' namespace
            └── module3.js # 'nested/module3' namespace

# Pinia equivalent, note ids match previous namespaces
src
└── stores
    ├── index.js          # (Optional) Initializes Pinia, does not import stores
    ├── module1.js        # 'module1' id
    ├── nested-module2.js # 'nestedModule2' id
    ├── nested-module3.js # 'nestedModule3' id
    └── nested.js         # 'nested' id
  • Pinia에서 namespace 는 id 생성과 동일한 기능을 한다.
  • 디렉토리 이름은 일반적으로 stores 라고 한다. 왜냐하면 피니아는 여러개의 store를 사용하기 때문에 그 사실을 강조하기 위함이다.

    대규모 프로젝트에서는 한꺼번에 모두 변환하는 것보다 모듈 단위로 변환하는 것이 좋을 수 있다. 실제로 pinia와 vuex를 섞어서 쓸 수 있고 migration 중에는 vuex와 pinia 둘다 접근해야하기 때문에 stores라고 명명 이유도 있다.


step by step

  1. id를 짓는다. 이전의 namespace와 동일하기 짓는걸 추천한다. 왜냐하면 mapStores() 사용 시 유리하다.
  2. state 를 function으로 변경한다.
  3. getters 변경
    1. 같은 이름으로 상태(state)를 반환하는 게터(getters)를 제거하세요 (예: firstName: (state) => state.firstName). 이것은 스토어 인스턴스에서 직접 모든 상태에 액세스할 수 있기 때문에 필요하지 않습니다.
    2. 화살표 함수를 쓰면 this 를 쓸 수 없으니 일반 function을 사용해라.
    3. rootState 또는 rootGetters 를 인자로 사용하는 대신 store를 직접 import해라.
  4. actions 변경
    1. context 인자를 삭제해라. this 로 접근할 수 있다.
    2. 다른 store 를 사용하려면 store파일을 직접 import 해라
  5. mutations 변경
    1. Pinia에는 더이상 mutations가 존재하지 않는다. 모두 actions에 넣는다.
    2. 만약 actions에 넣었다면 state 인자는 삭제하고 this 바인딩을 이용해서 접근해라
    3. 빌트인 function인 $reset메서드를 이용해 공통으로 사용할 mutations 값을 초기값으로 변경할 수 있다. 주의할 점으론 이 기능은 option stores에만 존재한다.
This post is licensed under CC BY 4.0 by the author.