개요
vue 로 개발하면서 자손에서 조상으로 데이터를 전달해야하는 일이 있었다.
근데 컴포넌트가 너무 많이 중첩되어 있어 $emit() 을 여러번 사용해야 겨우 조상으로 가져올 수 있었다.
전역변수를 설정하는 법은 찾아보면 많긴한데, 그중 pinia 라는 상태 관리 라이브러리를 사용했다.
단순히 값만 저장하는 변수가 아닌 반응형으로 만들 수 있기 때문이다.
설치
1
npm install pinia
pinia 의 구조
- State
- Getters
- Actions
- State
상태 값을 의미한다. stateful, stateless 할 때, 그 state 와 동일한 개념이다. 변수도 일종의 state 라고 볼 수 있다. - Getters
state 를 활용해 값을 미리 계산하는 속성. computed 라고 생각하면 된다. - Actions
state 를 활용하는 methods 라고 생각하면 된다.
Store 정의
store 는 state 를 저장하는 저장소를 의미한다.
단순히 state 만 저장하지 않고 getters 와 actions 도 같이 저장할 수 있다.
1
2
3
4
import {defineStore} from 'pinia'
export const useAlertsStore = defineStore('alerts', {
})
store 를 정의하려면 pinia 에서 제공하는 defineStore() 함수를 사용한다.
첫번째 인자는 만들고자 하는 store 의 이름을 작성해준다.
이름은 store 와 devtools 와 연결해주는 id 역할도 한다.
그리고 반환된 함수의 이름은 use + 이름 + Store 라고 짓는게 관례다.
위에서 정의한 store 는 state 와 getters 가 없다.
두번째 인자에 전달해주면 된다.
Option Store
store 의 두번째 인자에 options 객체를 전달해주는 방법이다.
1
2
3
4
5
6
7
8
9
10
11
12
// @/stores/counter.js
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo'}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
option 객체를 사용해 store 를 정의하는 것의 장점은 기존의 option API 사용법과 99% 유사해 쉽다는것이다.
state 를 date() 로, getters 를 computed 로, actions 를 methods 로 치환하면 거의 똑같다.
아무튼 위 코드를 사용해 store 를 정의하면, count 와 name 이라는 state 를 보관하는 store 를 만들 수 있다.
이제 count 가 필요하다면 vue 의 어디서든지 useCounterStore 를 호출해 count 를 꺼낼 수 있다.
Setup 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 }
})
setup 함수의 문법을 사용해 store 를 정의할 수 있다.
문법만 setup 과 동일한게 아니라 setup store 는 options store 에 비해 더욱 유연하고 강력한 기능을 제공한다.
하지만 앞으로 설명할 내용은 option store 방식을 기준으로 작성되어있다.
Store 사용
위에선 store 를 정의했을뿐 사용할 수 있는것이 아니다.
애초에 store 를 정의할 때, 이름을 useCounterStore 라고 지어줬다. 즉 counter store 사용하기 라는 이름인 것이고 이걸 호출해야 진짜 store 가 나온다.
1
2
3
4
5
6
7
8
9
10
// main.js
import { createApp } from 'vue'
import {createPinia} from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/stores(생성)/counter.js(생성)
import {defineStore} from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo'}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
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
26
27
// App.vue
<template>
store 테스트 합니다.<br>
<br />
{{ store }}<br />
<br />
store.count : {{ store.count }} <br />
store.name: {{ store.name }}
</template>
<script>
import { useCounterStore } from '@/stores/counter';
export default {
name: 'App',
components: {
},
data() {
const store = useCounterStore();
store.increment();
return {
store
}
}
}
</script>
store.increment() 를 수행했으므로 1이 정상적으로 출력되는 것을 확인할 수 있다.
State 초기화
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// App.vue
<template>
store 테스트 합니다.<br>
<br />
{{ store }}<br />
<br />
store.count : {{ store.count }} <br />
store.name: {{ store.name }}<br />
<br />
<button @click="clickIncrease">Increase</button><br/>
<br/>
<!-- 리셋 버튼을 만들어서 클릭해보자 -->
<button @click="clickReset">Reset</button><br/>
</template>
<script>
import { useCounterStore } from '@/stores/counter';
export default {
name: 'App',
components: {
},
data() {
const store = useCounterStore();
store.increment();
return {
store
}
},
methods: {
clickIncrease() {
this.store.increment();
},
// 이 함수가 호출되면 store 의 state 가 초기화된다.
clickReset() {
this.store.$reset();
}
}
}
</script>
state 의 값을 초기화하기 위해선 $reset() 을 호출한다.(option store 방식)
mapState
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
store 테스트 합니다.<br>
<hr />
{{ store }}<br />
<br />
store.count : {{ store.count }} <br />
store.name: {{ store.name }} <br />
<br/>
<button @click="clickIncrese">Increase</button><br />
<br />
<button @click="clickReset">Reset</button><br />
<hr />
count : {{ count }}<br />
doubleCount : {{ doubleCount }}<br />
myCount : {{ myCount }}<br />
triple : {{ triple }}<br />
magic value : {{ magicValue }}<br />
<button @click="clickCount">Count</button><br /><br />
</template>
<script>
import { mapState } from 'pinia';
import { useCounterStore } from '@/stores/counter';
export default {
name: 'App',
components: {
},
computed: {
...mapState(useCounterStore, ['count', 'doubleCount']),
...mapState(useCounterStore, {
myCount: 'count',
triple: store => store.count * 3,
magicValue(store) {
return store.count + this.triple;
}
}),
},
data() {
const store = useCounterStore();
return {
store
}
},
methods: {
clickIncrese() {
this.store.increment();
},
clickReset() {
this.store.$reset();
},
clickCount() {
this.count++;
}
}
}
</script>
mapState 는 state 를 변수에 mapping 하는 함수다.
...mapState(useCounterStore, ['count']), 라고 작성하면, count 변수에 store.count 를 mapping 해준다.
template 의 count : {{ count }}<br /> 가 아주 정상적으로 출력이되며 store.count 값과 공유되는것을 확인할 수 있다.
두번째 인자에 배열을 사용할 때는 변수명을 변경할 수 없다. state 의 이름이 count 이면 변수명은 반드시 count 를 사용해야한다.
만약 변수명을 지정하고 싶다면 아래처럼 작성하면 된다.
1
2
3
4
5
6
7
...mapState(useCounterStore, {
myCount: 'count',
triple: store => store.count * 3,
magicValue(store) {
return store.count + this.triple;
}
}),
store.count 라는 state 를 myCount 라는 변수에 mapping 할 수 있게된다.
뿐만 아니라 getters(computed), actions(methods) 를 작성할 수 있다.
triple 이라는 변수에 store => store.count * 3 이라는 새로운 getters 를 작성해 할당한 모습을 볼 수 있고, magicValue(store) 라는 action 을 새로 작성한것도 확인할 수 있다.
clickCount() 함수를 호출하면 count 가 증가하지 않는것을 확인할 수 있다.
이는 mapState() 가 읽기전용 속성으로 mapping 해주기때문이다.
만약 수정할 수 있게 하려면 mapState() 대신 mapWritableState() 를 사용해야한다.
사용법은 완전히 동일하다. mapState() 를 지우고 mapWritableState() 를 작성하면된다.
mapWritableState() 로 변경 후 clickCount() 함수를 호출해보자. 잘 작동할것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { mapWritableState } from 'pinia';
export default {
computed: {
...mapWritableState(useCounterStore, {
myCount: 'count',
// triple 작동 안함
triple: store => store.count * 3,
// magicValue 작동 안함
magicValue(store) {
return store.count + this.triple;
}
}),
}
}
대신 triple 이나 magicValue 같은 함수들은 사용이 불가능하고 state만 mapping 할 수 있다.
state 값 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
clickCount() {
// this.count++;
this.store.count++;
},
// 방법 1 : 매개변수로 state 객체를 전달
// { count: count + 1, name: 'DIO' } 객체를 매개변수로 전달
clickPatch() {
this.store.$patch({
count: this.store.count + 1,
name: 'DIO',
// items: items.push({ name: 'shoes', quantity: 1 }) 불가능
});
},
// 방법 2 : 매개변수로 익명 함수를 사용
clickPatch2() {
this.store.$patch( state => {
const x = this.count * this.triple + this.magicValue; // 어떤 로직
state.count = x;
state.count++;
state.name = 'DIO';
});
}
state 의 값을 변경할 때는 $patch 메서드를 사용한다.
clickCount() 처럼 state 를 직접 조작할 수도 있지만 $patch 사용을 권장한다.
이 방법의 장점은 간편하고 직관적이다.
단점은 변경해야하는 것이 많은 경우 devtools 로 추적이 어렵다는 점이다.
clickCount() 처럼 한 두개 정도 변경하는것이면 문제가 안되는데, 만약 100개의 state를 조작한다면 어떨까?
100 개의 변경사항이 개별적인 변경사항으로 기록되어 뿔뿔이 흩어져 기록될 수가 있다.
만약 이해가 안되도 괜찮다. $patch 설명을 들으면 이해가 될 수있다.
$patch() 의 사용 방법은 2 가지다.
변경할 state 들을 객체로 만들어 전달하는 방법과 함수로 묶어서 사용하는 방법이다.
방법 1 의 경우 간편하지만 복잡한 조작이 어렵다. 예를 들어 items.push() 같은 경우 객체 내부에서 사용할 수 없다.
또한 방법 2 에 나와있는 것처럼 어떤 로직이 필요한 경우 방법 1 에선 사용할 수 없다.
$patch() 를 사용하는 이유중 하나는 변경 history 가 patch 단위로 기록된다는 것이다.
patch 도중 인터럽트가 들어와 다른 로직이 수행되고 다시 patch 가 마무리 되면 변경 history 가 한번에 기록이 된다는 의미다.
만약 patch 를 사용하지 않고 this.store.count++; this.store.name = ‘DIO’ 를 사용했다면, history 에 count++ 작성되고, name = “DIO” 이렇게 작성될것이다.
만약 중간에 다른 기능이 수행되면? 두 history 는 떨어지게 될 것이다.
$patch() 를 사용하는 두번째 이유는 코드 일관성이 유지되어 유지보수하기 용이해진다.
state 를 변경하는 로직에 모두 patch 를 사용하면 나중에 patch 를 검색해서 찾아갈 수 있다.
state 변화 감시(구독)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const store = useCounterStore();
store.$subscribe( (mutation, state) => {
console.log("store 변화가 감지되었습니다.");
console.log("mutation.type: " + mutation.type);
console.log("mutation.storeId: "+ mutation.storeId);
console.log("mutation.payload: " + mutation.payload);
console.log(state);
});
// 또는
store.$subscribe( (mutation, state) => {
console.log("store 변화가 감지되었습니다.");
console.log("mutation.type: " + mutation.type);
console.log("mutation.storeId: "+ mutation.storeId);
console.log("mutation.payload: " + mutation.payload);
console.log(state);
}, { flush: 'sync' });
store의 변화를 감시하려면 $subscribe( ) 메서드를 사용한다.
값의 변화가 감지되면 수행할 동작을 콜백함수로 작성해준다.
pinia 는 객체의 상태가 변화할때마다 mutation 객체를 만들어서 넘겨주는데, 이것으로 다음 정보를 확인할 수 있다.
- mutation.type store 의 변경 방식에 대해 알려준다.
- direct : 직접 수정. store.count++ 과 같이 직접 조작한 경우다.
- patch function : $patch( state => { count = count + 1 } ) 를 이용한 경우다.
- patch object : $patch( { count: count + 1 } ) 를 이용한 경우다. $patch 를 사용할때 객체를 사용했으면 patch object 라고 알려주고 함수를 사용해 조작했으면 patch function 이라고 알려준다.
mutation.storeId 변화가 감지된 store 의 id 를 알려준다.
위 코드 같은 경우 id 가 ‘counter’ 가 출력된다.- mutation.payload mutation.type 이 patch object 인 경우에만 출력된다.
patch 로 전달된 object 를 알려준다.
$patch( { count: count + 1 } ) 를 했으면 mutation.payload 는 { count: count + 1 } 가 된다.
$subscribe( ) 는 내부적으로 vue 의 watch( ) 를 사용하기 때문에 flush 값을 watch 로 전달해줄 수 있다.
flush 는 default 값이 있기 때문에 생략해도 된다.
구독 해제
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<!-- 생략 -->
<button @click="clickPatch">Patch</button><br /><br />
<button @click="clickUnsubscribe">Unsubscribe</button><br /><br />
</template>
<script>
export default {
name: 'App',
components: {
},
//생략
,
data() {
const store = useCounterStore();
const unsubscribe = store.$subscribe( (mutation, state) => {
console.log("store 변화가 감지되었습니다.");
console.log("mutation.type: " + mutation.type);
console.log("mutation.storeId: "+ mutation.storeId);
console.log("mutation.payload: " + mutation.payload);
console.log(state);
});
return {
store, unsubscribe
}
},
methods: {
// 생략
clickPatch() {
this.store.$patch( state => {
const x = this.count * this.triple + this.magicValue;
state.count = x;
state.count++;
state.name = 'DIO';
});
// this.store.$patch({
// count: this.store.count+1,
// hasChanged: true,
// })
},
clickUnsubscribe() {
this.unsubscribe(); // 구독 해제
}
}
}
</script>
const unsubscribe = store.$subscribe( )
$subscribe( ) 는 구독 해제 함수 를 리턴한다.
즉 리턴된 함수를 사용하면 구독이 해제된다.
unsubscribe 버튼을 클릭하기 전 후로 patch 버튼을 클릭해 비교해보면 확인할 수 있다.