99

최근 30분간 동시 방문자 수를 표시합니다. (~)

최고 동시 방문자 수 -
어제: 0명 / 오늘: 0명

Pinia 핵심 정리

Pinia 핵심 정리

info

이 글은 Pinia 3.0.2 버전을 기준으로 작성되었습니다.

# 개요

Pinia는 Vue 애플리케이션의 상태 관리를 위한 라이브러리입니다.
Vuex 다음 버전의 논의 아이디어가 반영된 새로운 라이브러리로 시작해서 2022년부터 Vue의 공식 상태 관리 라이브러리가 되었습니다.
Vuex의 변이(Mutations)가 사라지면서 훨씬 쉽게 데이터 변경이 가능하고, 특히 Composition API와 TypeScript에 친화적입니다.
새로운 Vue 프로젝트를 시작한다면, Vuex가 아닌 Pinia를 사용이 적극 권장됩니다.

# 설치 및 구성

다음과 같이 Pinia를 설치합니다.

BASH
content_copy
1
npm i pinia

프로젝트에서 사용하기 위해 다음과 같이 Vue 플러그인으로 등록합니다.

/src/main.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const pinia = createPinia() createApp(App) .use(pinia) .mount('#app')

# 스토어 생성

관리할 스토어는 defineStore 함수를 호출해 생성합니다.
이 함수는 스토어 인스턴스를 식별하는 고유한 문자(스토어 ID)와 상태, 게터, 액션 정의(스토어 객체)를 인수로 받습니다.
또한 defineStore 함수의 반환은 스토어 인스턴스를 얻을 수 있는 팩토리 함수입니다.
이 함수를 보통 훅(Hook)이라고 칭하며, use 접두사와 Store 접미사로 작명합니다.
그리고 컴포넌트나 외부에서 호출해 사용할 수 있습니다.

TS
content_copy
1
2
3
import { defineStore } from 'pinia' export const use이름Store = defineStore('스토어_ID', 스토어_객체)

숫자를 다루는 스토어(Count Store)를 예시로 살펴봅시다.
모듈화를 위해 /src/stores/count.ts 파일을 생성하고 다음과 같이 스토어를 생성합니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({}), getters: {}, actions: {} })

# 상태

상태(State)는 스토어에서 정의하는 반응형 데이터입니다.
스토어 객체의 state 옵션에서 정의하며, 꼭 팩토리 함수로 작성해야 합니다.
이는 스토어 인스턴스가 여러 번 생성되더라도 상태가 불필요하게 공유 혹은 초기화되는 문제를 방지하기 위함입니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ count: 1, double: 2, negativeDouble: -2, isNegative: false, history: [] as number[] }) })

정의한 각 상태는 스토어 인스턴스에서 바로 조회할 수 있습니다.
스토어 인스턴스는 useCountStore()와 같이 훅을 호출해 얻을 수 있습니다.

/src/App.vue
VUE
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts"> import { useCountStore } from '@/stores/count' const countStore = useCountStore() </script> <template> <h2>Count: {{ countStore.count }}</h2> <h2>Double: {{ countStore.double }}</h2> <h2>Negative Double: {{ countStore.negativeDouble }}</h2> <h2>Is Negative: {{ countStore.isNegative }}</h2> <h2>History: {{ countStore.history }}</h2> </template>

# 게터

게터(Getters)는 상태를 기반으로 계산된 값을 반환하는 함수로써, 게터라는 이름 그대로 읽기 전용입니다.

다음 예제에서 doubleisNegativecount 상태를 기반으로 계산된 값을 반환합니다.
이때 게터 함수의 첫 매개변수로 상태 객체를 얻을 수 있습니다.
그런데 negativeDouble의 경우는 이제 double 게터의 값을 기반으로 계산해야 하지만, 화살표 함수를 사용할 때는 다른 게터에 접근할 수 있는 방법이 없습니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ count: 1, history: [] as number[] }), getters: { double: state => state.count * 2, isNegative: state => state.count < 0 // negativeDouble: () => this.double * -1 // 정상적으로 동작하지 않음! } })

그래서 만약 게터가 다른 게터의 값을 기반으로 계산하려면, 화살표 함수 대신 일반 함수를 사용하고 this 키워드를 통해 상태나 다른 게터에 접근할 수 있습니다.
그런데 이렇게 this 키워드를 사용할 때는 게터의 반환 타입을 꼭 명시해야 합니다.

info

일반 함수와 화살표 함수의 this 키워드 차이를 이해하면, 현재 구조에서 화살표 함수로 this 키워드를 사용할 수 없는 이유를 알 수 있습니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { // ... getters: { double: state => state.count * 2, isNegative: state => state.count < 0, negativeDouble(): number { return this.double * -1 } } })

# 액션

액션(Actions)은 상태나 게터를 활용해 실행할 수 있는 함수입니다.
this 키워드로 상태나 게터 그리고 다른 액션에 접근할 수 있으며, 상태를 변경할 수도 있습니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ count: 1, history: [] as number[] }), getters: { double: state => state.count * 2, isNegative: state => state.count < 0, negativeDouble(): number { return this.double * -1 } }, actions: { increase(value = 1) { this.count += value this.history.push(this.count) }, decrease(value = 1) { this.count -= value this.history.push(this.count) } } })

다음과 같이 스토어 인스턴스에서 바로 접근해서 호출할 수 있습니다.

/src/App.vue
VUE
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> import { useCountStore } from '@/stores/count' const countStore = useCountStore() </script> <template> <button @click="countStore.increase">증가!</button> <button @click="countStore.decrease">감소!</button> <h2>Count: {{ countStore.count }}</h2> <h2>Double: {{ countStore.double }}</h2> <h2>Negative Double: {{ countStore.negativeDouble }}</h2> <h2>Is Negative: {{ countStore.isNegative }}</h2> <h2>History: {{ countStore.history }}</h2> </template>

다음과 같이 비동기 액션을 사용할 수도 있습니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { // ... actions: { // ... async fetchCount() { const res = await fetch('https://api.heropy.dev/v0/count') const count = await res.json() this.count = count } } })

# 인스턴스 멤버

스토어 인스턴스에서 제공하는 여러 멤버를 통해 스토어를 더욱 다양하게 활용할 수 있습니다.

# 스토어 ID 확인 ($id)

defineStore의 첫 번째 인수로 제공한 스토어 인스턴스를 식별하는 고유한 문자를 $id 속성으로 얻을 수 있습니다.

/src/App.vue
VUE
content_copy
1
2
3
4
5
6
<script setup lang="ts"> import { useCountStore } from '@/stores/count' const countStore = useCountStore() console.log(countStore.$id) // 'count' </script>

# 상태 객체 확인 ($state)

$state 속성으로 스토어 인스턴스의 상태 객체에 접근할 수 있습니다.

content_copy
1
2
3
4
5
6
<script setup lang="ts"> import { useCountStore } from '@/stores/count' const countStore = useCountStore() console.log(countStore.$state) // { count: 1, history: [] } </script>

# 상태 변경 ($patch)

컴포넌트에서는 상태의 읽기와 쓰기가 모두 가능합니다.
만약 여러 상태를 한 번에 변경하는 경우, 각각의 반응성 트리거로 불필요한 렌더링이 발생할 수 있습니다.
이때 $patch 메소드를 사용하면 여러 상태 변경을 단일 작업으로 처리할 수 있습니다.
이를 통해 개별적인 반응성 트리거를 방지하고, 명시적으로 상태 변경을 처리하므로 가독성을 높입니다.

다음 예제에서 handleClick 함수는 여러 상태를 개별적으로 변경하고 있습니다.

/src/App.vue
VUE
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts"> import { useCountStore } from '@/stores/count' const countStore = useCountStore() function handleClick() { if (countStore.count > 100) { countStore.count = 100 countStore.history.push(100) } else { countStore.count += 1 countStore.history.push(countStore.count) } } </script> <template> <button @click="handleClick">트리거!</button> <h2>Count: {{ countStore.count }}</h2> <h2>History: {{ countStore.history }}</h2> </template>

이때 다음과 같이 $patch 메소드를 사용해 여러 상태를 한 번에 변경할 수 있습니다.
이 방식은 간단한 데이터 변경에는 편리하지만, 기존 값을 참조하거나 값이 일부만 변경(참조형)하는데는 추가 비용이 듭니다.

TS
content_copy
1
2
3
4
store.$patch({ 상태1: 값, 상태2: 값 })
객체로 변경
/src/App.vue
VUE
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts"> // ... function handleClick() { if (countStore.count > 100) { countStore.$patch({ count: 100, history: [...countStore.history, 100] }) } else { countStore.$patch({ count: countStore.count + 1, history: [...countStore.history, countStore.count + 1] }) } } </script>

이를 보완하기 위해 $patch 메소드는 다음과 같이 함수 형태로도 사용할 수 있습니다.
콜백의 매개변수로 상태 객체를 전달받아 각 세부 상태를 자유롭게 변경할 수 있습니다.

TS
content_copy
1
2
3
4
store.$patch(state => { state.상태1 = 값 state.상태2 = 값 })
함수로 변경
/src/App.vue
VUE
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts"> // ... function handleClick() { countStore.$patch(state => { if (countStore.count > 100) { state.count = 100 state.history.push(100) } else { state.count += 1 state.history.push(state.count) } }) } </script>

스토어 멤버는 액션 내에서도 사용할 수 있습니다.
this 키워드로 접근합니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { // ... actions: { increase(value = 1) { this.$patch(state => { state.count += value state.history.push(state.count) }) }, decrease(value = 1) { this.$patch(state => { state.count -= value state.history.push(state.count) }) } } })

# 상태 구독 ($subscribe)

$subscribe 메소드로 스토어의 상태가 변경될 때마다 콜백을 실행할 수 있습니다.
주로 상태의 변화에 맞게 로컬 스토리지에 저장하거나, 외부로 동기화할 때 사용합니다.

TS
content_copy
1
const 구독해제함수 = store.$subscribe(콜백, 옵션?)

콜백은 변경 정보(mutation)와 변경된 상태 객체(state)를 인수로 받습니다.
대표적으로 변경 정보의 type 속성을 통해서 상태가 직접 수정('direct')되었는지, $patch 메소드로 변경('patch object' | 'patch function')되었는지 구분할 수 있습니다.

TS
content_copy
1
2
3
4
5
6
7
countStore.$subscribe((mutation, state) => { console.log(mutation, state) // 변경 정보, 변경된 상태 객체 console.log(mutation.type) // 'direct' | 'patch object' | 'patch function' console.log(mutation.storeId) // 스토어 ID console.log(mutation.events.oldValue) // 변경 전 값 console.log(mutation.events.newValue) // 변경 후 값 })

$subscribe 메소드에서는 다음과 같이 Vue watch() 옵션을 사용할 수도 있습니다.
그리고 메소드의 반환 값은 구독을 해제할 수 있는 함수입니다.

TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
const unsubscribe = countStore.$subscribe( (mutation, state) => { // ... }, { immediate: true, deep: true, flush: 'sync' // ... } ) unsubscribe() // 구독 해제!

# 액션 구독 ($onAction)

스토어의 액션 호출을 구독하여 액션이 실행될 때 콜백을 실행합니다.
액션 실행 전후의 동작을 감지하거나 로깅할 때 유용합니다.

TS
content_copy
1
2
3
4
5
6
7
8
const unsubscribe = countStore.$onAction(payload => { console.log(payload.name) // 액션 이름 console.log(payload.args) // 액션 호출의 인수 console.log(payload.after) // 액션 호출의 return 혹은 resolve 후 실행할 함수 console.log(payload.onError) // 액션 호출의 throw 혹은 reject 시 실행할 함수 }) unsubscribe() // 구독 해제!

# 상태 초기화 ($reset)

$reset 메소드를 호출하면 변경된 모든 상태를 초깃값으로 되돌립니다.

/src/App.vue
VUE
content_copy
1
2
3
<template> <button @click="countStore.$reset">모든 상태 초기화!</button> </template>

만약 개별 상태를 초기화해야 하는 경우, 별도의 액션을 작성해야 합니다.

/src/stores/count.ts
TS
content_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ count: 1, history: [] as number[] }), // ... actions: { // ... resetCount() { this.count = 0 }, resetHistory() { this.history = [] } } })

# 스토어 폐기 ($dispose)

스토어를 완전히 제거하고 메모리에서 해제합니다.
스토어가 더 이상 필요 없을 때 사용할 수 있습니다.

/src/App.vue
VUE
content_copy
1
2
3
<template> <button @click="countStore.$dispose">스토어 폐기!</button> </template>