Pinia, 피니아를 알아보자

피니아 vue 중앙 관리
Nov 13, 2023
Pinia, 피니아를 알아보자

스토어 정의하기

스토어는 defineStore() 를 사용해 정의 된다.
고유한 이름이 첫 번째 인자로 전달되어야 한다.
import { defineStore } from 'pinia'; // 첫 번째 인자는 스토어의 고유 ID이다. export const useAlertsStore = defineStore('alerts', { // 다른 옵션 })

옵션 스토어

vue의 옵션 API와 유사하게 state, actions 및 getters 속성을 사용하여 옵션 객체를 전달할 수 있다.
state === data
getters === computed
actions === methods
위 처럼 각각 스토어에 비슷하게 사용이 가능하게 매칭이 된다. 옵션 스토어는 시작하기 쉽고 직관적이다.
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: 'Eduardo' }), getters: { doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++ }, }, })

셋업 스토어

스토어를 정의하는 또 다른 문법이다. vue 컴포지션 api의 셋업 함수와 유사하게, 반응형 속성 및 메서드를 정의하고, 노출하려는 속성 및 메서드가 있는 객체를 반환하는 함수를 전달할 수 있다.
export const useCounterStore = defineStore('counter', () => { // ref는 state 속성 const count = ref(0) const name = ref('Eduardo') // computed는 getters이다. const doubleCount = computed(() => count.value * 2) // function은 actions가 된다. function increment() { count.value++ } return { count, name, doubleCount, increment } })
중요한 것은 Composition API 에서 사용할 때라는 것이다.
  • refstate 속성
  • computed()getters 속성
  • function()actions가 된다.
셋업 스토어는 옵션 스토어보다 자유롭다. 유연성이 높다.

우리 회사에서 채택하여 사용하는 방식은? 옵션 스토어 방식을 사용하고 있었다.


스토어 이용하기

스토어는 <script setup> 구성요소 내에서(또는 모든 컴포저블과 마찬가지로 setup() 내에서) use...Store()가 호출될 때까지 스토어가 생성되지 않기 때문에 스토어를 정의 한다.
<script setup> import { useCounterStore } from '@/stores/counter' // 즉 이부분에서 호출 되기 전까지 생성되지 않는다!!!! const store = useCounterStore() </script>

저장소에서 디스트럭처링

→ 디스트럭처링(구조 분해 할당) : 각 요소를 추출하여 리스트에 할당한다.
  • 배열의 경우 배열의 인덱스
  • 객체의 경우 프로퍼티 이름(키)
반응형을 유지하면서 스토어에서 속성을 추출하려면, storeToRefs()를 사용해야한다.
모든 반응형 속성에 대한 참조를 생성한다.
→ 이것은 스토어의 상태만 사용하고, 액션을 호출하지 않을 때 유용하다.
스토어 자체에도 바인딩되므로, 스토어에서 직접 액션을 구조화할 수 있다.
<script setup> import { storeToRefs } from 'pinia'; const store = useCounterStore(); // `name`과 `doubleCount`는 반응형 refs임. // 이것은 플러그인에 의해 추가된 속성에 대한 'refs'도 추출함. // 그러나 모든 액션 또는 비반응형(ref/반응형이 아닌) 속성을 건너뜀. const { name, doubleCount } = storeToRefs(store) // increment 액션은 그냥 구조화 가능. // 위에서 function increment() 으로 설정해 놨었음 const { increment } = store </script>
스토어에서 직접 액션을 구조화할 수 있습니다 위에 액션을 function increment()로 설정해 놧었기에 바로 구조화할 수 있다고한다.

State(상태)

상태는 대부분은, 스토어를 중심으로 이루어진다.
일반적으로 앱을 나타내는 상태를 정의하는 것으로 시작한다.
피니아에서 상태는 초기 상태를 반환하는 함수로 정의된다. 이를 통해 피니아는 서버 측과 클라이언트 측 모두 작동할 수 있다.
import { defineStore } from 'pinia'; export const useStore = defineStroe('storeId', { state: () => { return { count: 0, nume: 'younyc', isAdmin: true, items: [], hasChanged: true, } } });

state에 접근

기본적으로 store 인스턴스로 상태에 접근하여 상태를 직접 읽고 쓸 수 있다.
const store = useStore(); store.count++;
만약 state()에 상태를 정의해 두지 않았다면, 새 상태 속성을 추가할 수 없습니다.
예를 들어 state()에 secondCount가 정의되어 있지 않으면, state.secoundCount = 2를 수행할 수 없습니다.

상태 재설정

옵션 스토어에서는 $reset() 메소드를 호출하여 상태를 초기 값으로 재설정 할 수 있다.
const store = useStore(); store.$reset();

상태 변경하기

store.count++ 로 스토어를 직접 변경하는 방법 외에도, $patch 메소드를 호출할 수 도 있다.
이것을 이용하여 state 객체의 일부분을 동시에 변경할 수 있다.
store.$patch({ count: store.count + 1, age: 120, name: 'DIO', })
일부 mutations는 이러한 문법으로 적용하기 어렵다. 이 때문에 $patch 메소드는 패치 객체로 적용하기 어려운 이러한 종류에 뮤테이션을 그룹화 하는 함수도 허용한다.
store.$patch((state) => { state.items.push({ name: 'shoes', quantity: 1 }) state.hasChanged = true })
여기서 주요 차이점은 $patch() 를 사용하여 devtools에서 여러 변경 사항을 하나의 항목으로 그룹화 할 수 있다는 것.

state 교체하기

반응성을 깨뜨릴 수 있으므로 스토어의 상태를 정확하게 교체할 수 없다.
그러나! 패치할 수 있다.
// 이것은 실제로 `$state`를 교체하지 않음. store.$state = { count: 24 } // 아래와 같이 내부적으로 `$patch()`를 호출함: store.$patch({ count: 24 })

상태 구독하기

스토어 $subscribe() 메소드를 통해 상태의 변경 사항을 감시할 수 있습니다.
일반 watch() 보다 $subscribe() 사용시 장점은 구독이 여러 패치 이후에 한 번만 트리거 된다는 것이다.
→ 이 때 여러 패치란? : $patch() 내부에 함수를 여러번 패치가 진행 되게 해놧다해도 결국 한번만 돈다는 것?
cartStore.$subscribe((mutation, state) => { // import { MutationType } from 'pinia' mutation.type // 'direct' | 'patch object' | 'patch function' // `cartStore.$id`와 동일. mutation.storeId // 'cart' // `mutation.type === 'patch object'`에서만 사용 가능. mutation.payload // cartStore.$patch()에 전달된 패치 객체 // 변경될 때마다 전체 상태를 로컬 스토리지에 유지 localStorage.setItem('cart', JSON.stringify(state)) })
기본적으로 상태 구독은 컴포넌트에 추가된(스토어가 컴포넌트의 setup() 내부에 있는) 경우에 바인딩된다. 따라서 컴포넌트가 마운트 해제 되면 자동으로 제거된다.
 
이때 마운트 해제 된 후에도 이를 유지하고 싶다면, 두 번째 인수로 현재 컴포넌트에서 상태 구독을 분리하는 { detached: true }를 전달해야한다.
someStore.$subscribe(callback, { detached: true })

Getters(게터)

게터는 스토어의 상태에 대한 계산된 값과 정확하게 동일하다고 한다.
→ 계산된 값이란? : computed
defineStore() 내에서 getters 속성으로 정의할 수 있다.
화살표 함수 를 권장하기 위해, 첫 번째 인자로 state를 받습니다.
export const useCounterStore = defineStore('counter', { // defineStore로 옵션 스토어 방식으로 시작했다. state: () => ({ // state : data를 선언하여 초기화해주었다. 0으로 count: 0, }), getters: { // getters를 선언하여 아래 doubleCount라는 이름으로 함수를 만들었다. doubleCount: (state) => state.count * 2, }, })
대부분의 경우, 게터는 오직 상태(state)에만 의존하지만, 다른 게터를 사용해야할 수 있다.
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { // 자동으로 반환 유형을 숫자로 유추함. doubleCount(state) { return state.count * 2 }, // 반환 유형은 **반드시** 명시적으로 설정되어야 함. doublePlusOne(): number { // 전체 스토어에 대한 자동 완성 및 타이핑 ✨ // 자세히 보면 this. 으로 doubleCount에 접근하는 것을 확인할 수 있다. return this.doubleCount + 1 }, }, })

다른 getter에 접근

계산 된 속성(computed)처럼 여러 게터를 결합할 수 있다.
this를 통해 다른 게터에 접근한다.
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount: (state) => state.count * 2, doubleCountPlusOne() { // 자동완성 ✨ return this.doubleCount + 1 }, }, })

getter에 인자 전달

게터는 내부적으로 계산된 속성일 뿐이라 파라미터를 전달할 수 없다.
그러나 게터에서 함수를 반환하여 모든 인자를 받을 수 있다.
중요한 대목인듯 하다.
export const useStore = defineStore('main', { getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId) }, }, })
그리고 컴포넌트에서 사용
<script setup> import { storeToRefs } from 'pinia' import { useUserListStore } from './store' const userList = useUserListStore() const { getUserById } = storeToRefs(userList) // <script setup> 내에서 함수에 액세스하려면 // `getUserById.value`를 사용해야 합니다. </script> <template> <p>유저 2: {{ getUserById(2) }}</p> </template>
이 작업을 수행할 때 게터는 더 이상 캐시되지 않고, 단순히 호출하는 함수이다.
그러나 게터 자체 내부에 일부 결과를 캐시할 수 있다. 이는 흔하지 않지만 성능이 더 우수하다.

다른 스토어 getter에 접근

다른 스토어 게터를 사용하려면, 게터 내부에서 직접 사용할 수 있다.
import { useOtherStore } from './other-store' export const useStore = defineStore('main', { state: () => ({ // ... }), getters: { otherGetter(state) { // useOtherStore 를 불러온다. 그 후 접근할 수 있다. const otherStore = useOtherStore() return state.localData + otherStore.data }, }, })

setup() 에서 사용

<script setup> const store = useCounterStore() store.count = 3 store.doubleCount // 6 </script>
store에서 state 값을 변경하는 방법은 직접 변경과 getters를 이용한 방법 두가지다.

옵션 API에서 사용

// 예제 파일 경로: // ./src/stores/count.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { doubleCount(state) { return state.count * 2 } } })
위와 같이 저장소를 만들었다. 이제 사용해보자

setup()에서

<script> import { useCounterStore } from '../stores/counter' export default defineComponent({ setup() { const counterStore = useCounterStore() // 구조를 파괴하는 대신 전체 저장소만 반환합니다. return { counterStore } }, computed: { quadrupleCounter() { return this.counterStore.doubleCount * 2 }, }, }) </script>
컴포지션 API가 모든 사람을 위한 것은 아니지만, setup() 훅을 사용하면 옵션 API에서 피니아를 더 쉽게 사용할 수 있다.

setup() 없이

mapState() 함수를 사용하여 게터에 매핑할 수 있다.
import { mapState } from 'pinia' import { useCounterStore } from '../stores/counter' export default { computed: { // 컴포넌트 내부에서 `this.doubleCount`로 접근할 수 있게 함. // `store.doubleCount`로 읽는 것과 동일. ...mapState(useCounterStore, ['doubleCount']), // 위와 같지만 `this.myOwnName`으로 등록. ...mapState(useCounterStore, { myOwnName: 'doubleCount', // 스토어에 접근하는 함수를 작성할 수도 있음. double: (store) => store.doubleCount, }), }, }

Actions(액션)

액션은 컴포넌트의 메서드와 동일하다.
이들은 defineStore() 에서 actions 속성으로 정의할 수 있으며, 처리해야할 작업의 로직을 정의하는 데 완벽하다.
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), actions: { // `this`에 의존하므로, 화살표 함수를 사용할 수 없음. increment() { this.count++ }, randomizeCounter() { this.count = Math.round(100 * Math.random()) }, }, })
자 여기서 this에 의존하므로, 화살표 함수를 사용할 수 없다. 라는거 진짜 중요하다.
화살표 함수의 this 바인딩화살표 함수에는 this가 아예 없다.
화살표 함수에는 this라는 변수 자체가 존재하지 않기 때문에 그 상위 환경에서의 this를 참조하게 된다. 그렇기에 this에 의존할 경우에는 절대 화살표 함수를 쓰면 안된다.
 
다시 돌아와서 게터와 마찬가지로 액션은 전체 입력(및 자동 완성) 지원과 함께 this를 통해 전체 스토어 인스턴스에 접근이 가능하다.
 
게터와 달리 actions는 비동기 식일 수 있다. 액션 내에서 API 호출이나 다른 액션을 await할 수 있다. asyncawait 를 사용하면 더욱 안전하게 비동기를 사용할 수 있다.

다른 스토어 액션에 접근

액션 내부에서 직접 다른 스토어를 사용할 수 있다.
import { useAuthStore } from './auth-store' // import를 통해서 다른 스토어를 들고온다. export const useSettingsStore = defineStore('settings', { state: () => ({ preferences: null, // ... }), actions: { async fetchUserPreferences() { // 이와 같이 useAuthStore()를 쓰기 위해 아래와 같이 넣어준다. const auth = useAuthStore() // 이제 조건식을 걸어 접근한다. if (auth.isAuthenticated) { this.preferences = await fetchPreferences() } else { throw new Error('인증이 필요합니다!') } }, }, })

옵션 API에서 사용

저장소를 생성한다.
// 예제 파일 경로: // ./src/stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })

setup() 에서

<script> import { useCounterStore } from '../stores/counter' export default defineComponent({ setup() { const counterStore = useCounterStore() return { counterStore } }, methods: { incrementAndPrint() { this.counterStore.increment() console.log('숫자 세기:', this.counterStore.count) }, }, }) </script>
setup 내부에서 사용하게 되면 헬퍼함수가 필요하지 않다.

setup() 없이

import { mapActions } from 'pinia' import { useCounterStore } from '../stores/counter' export default { methods: { // 컴포넌트 내부에서 `this.increment()`로 접근할 수 있게 함. // `store.increment()`처럼 호출하는 것과 동일. ...mapActions(useCounterStore, ['increment']), // 위와 같지만 `this.myOwnName()`으로 등록. ...mapActions(useCounterStore, { myOwnName: 'increment' }), }, }
이럴 경우 …mapActions 헬퍼 함수를 꼭 사용해주어야한다.

액션 구독하기

store.$onAction() 에 콜백을 전달해 액션과 그 결과를 감시할 수 있다.
const unsubscribe = someStore.$onAction( ({ name, // 액션의 이름. store, // 스토어 인스턴스, `someStore`와 같음. args, // 액션으로 전달된 인자로 이루어진 배열. after, // 액션에서 반환 또는 해결 이후의 훅. onError, // 액션에서 실패 또는 거부될 경우의 훅. }) => { // 이 특정 액션 호출에 대한 공유 변수. // 역자설명: 이 액션의 훅에서 참조하게 되는 클로저 변수 개념. const startTime = Date.now() // 이곳은 `store`의 액션이 실행되기 전에 트리거됨. console.log(`"${name}"가 [${args.join(', ')}] 인자를 전달받아 시작됩니다.`) // 액션이 성공하고 완전히 실행된 후에 트리거됨. // 프라미스 반환을 대기. after((result) => { console.log( `"${name}"가 ${ Date.now() - startTime }ms 후 종료됬습니다.\n결과: ${result}.` ) }) // 액션이 실패하거나 프라미스가 거부되면 트리거 됨. onError((error) => { console.warn( `"${name}"가 ${ Date.now() - startTime }ms 후 실패했습니다.\n애러: ${error}.` ) }) } ) // 리스너를 수동으로 제거. unsubscribe()
기본적으로 액션 구독은 컴포넌트에 추가 된 경우에 바인딩 된다.
따라서 컴포넌트가 마운트 해제되면 자동으로 제거가 된다. 컴포넌트가 마운트 해제 된 후에도 이를 유지하려면, 두 번째 인수로 현재 컴포넌트에서 액션 구독을 분리하는 true를 전달하면 된다.
someStore.$onAction(callback, true)
 
이렇게 state에서 actions 까지 한번 알아보았다. 물론 기억이 나중에 안나겠지만 그때마다 보면서 복기해야겠다…
Share article
RSSPowered by inblog