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