음악, 삶, 개발

모든 Component 들간에 하나의 저장소 : Vuex 본문

개발 Web/Vue.js 공부방

모든 Component 들간에 하나의 저장소 : Vuex

Lee_____ 2020. 11. 28. 20:39

< 공식 문서 >

Vuex 4.0 공식문서


< Vuex 란?  : Global State >

아래와 같이 중첩된 요소들이 있다고 가정해보자.

<parent>

  <child>

    <grand-child>

      <grand-grand-child>

        <grand-grand-grand-child />

      </grand-grand-child>

    </grand-child>

  </child>

</parent>

우리가 만들 app 의 모습도 대략 이럴것이다.

이때 서로 데이터를 주고 받는 것을 생각해보자.

 

가장 부모인 parent 에서 가장 아래 자식인 grand-grand-grand-child 로 데이터를 전송해야한다면..

parent -> child -> grand-child -> grand-grand-child -> grand-grand-child

 순으로 props 를 넘겨가야한다.

 

역으로, 자식 -> 부모로 데이터를 전달하려면 emit 함수를 통해 event 를 보내야한다.

 

아래와 같은 상황도 고려해보자.

<parent>

  <brother/> 

  <sister>

    <sister-child/>

  </sister>

</parent>

brother 에서 sister-child 에게 데이터를 넘겨야한다면?

이둘은 부모-자식 관계가 아니다.

이때는 eventBus 를 사용해야한다.

 

이렇게 Vue 의 컴포넌트간에 데이터 주고받기가 조금 까다로운 이유는,

각 컴포넌트의 state 들이 local scope, 즉 local variable 같은 녀석들이기때문이다.

 

나의 컴포넌트가 1000개, 10000개가 되어간다면,

서로간에 데이터 주고받기는 더더욱 고통스러워질것이다.

이를 극복하기위해 나온것이 Vuex 이다.

 

Vuex 는 Global State 를 제공하는 npm 라이브러리이다.

Vue 팀에서 직접 만들어 유지보수하고있기때문에, 믿고 사용할수있다.

Vuex 를 사용하면 각 컴포넌트의 state 를 해당 컴포넌트의 local 에서 관리하는 대신,

Global 로써 상태를 저장하고, 이 상태가 변화했을때 컴포넌트가 다시 랜더링된다.

(JuceValueTree 같은 녀석이다)


< 설치방법 >

npm install vuex@4.0.0-rc.2

현재 release candidate 상태라, @버전명을 붙여 위와 같이 작성한다.

추후 정식으로 release 되면 npm install vuex 로 설치가능할것이다.

반드시 vuex 3 아닌 vuex 4 를 설치해야한다.

vuex 4vue 3 와 호환되기때문이다. (composition api 관련)


< 사용법 >

/* store.js */

import { createStore } from 'vuex'

const store = createStore({

    state       : { count : 0},
    mutations   : {

        increment(state) { state.count++ },
        decrement(state) { state.count-- },

    }

})

export default store

또는,

/* store.js */

import { createStore } from 'vuex'

const state     = { count : 0 }
const mutations = {

    increment(state) { state.count++ },
    decrement(state) { state.count-- },

}

const store = createStore({ state, mutations })

export default store
/* main.js */

import { createApp } from 'vue'
import { store } from './store/store.js'
import App from './App.vue'

const app = createApp(App)

app.use(store)
app.mount('#app')
<!-- App.vue -->

<template>
  
  {{ store.state.count }}

</template>

<script>

  import { useStore } from 'vuex'

  export default {
    
    setup() {

      const store = useStore()

      return { store }

    }
    
  }

</script>

< 예제 코드 >

/* store.js */

import { createStore } from 'vuex'

const state = { 
    
    count : 1 

}

const getters = {

    nextCount       (state)                         { return state.count + 1 },
    nextNextCount   (state, getters)                { return getters.nextCount + 1},
    getSumCount     : (state) => (numberToSum) =>   { return state.count + numberToSum }

}

const mutations = {

    increment   (state)             { state.count++ },
    decrement   (state)             { state.count-- },
    add         (state, payload)    { state.count += payload.amount },
    sub         (state, payload)    { state.count -= payload.amount }

}

const actions = {

    increment(context) { context.commit('increment') }

}

const store = createStore({ 
    
    state       : state, 
    mutations   : mutations, 
    getters     : getters, 
    actions     : actions

})

export default store
/* main.js */

import { createApp } from 'vue'
import { store } from './store/store.js'
import App from './App.vue'

const app = createApp(App)

app.use(store)
app.mount('#app')
<!-- App.vue -->

<template>

  <!-- state  -->
  <div>{{ store.state.count           }}    </div>

  <!-- getters  -->
  <div>{{ store.getters.nextCount     }}    </div>
  <div>{{ store.getters.nextNextCount }}    </div>
  <div>{{ store.getters.getSumCount(100)}}  </div>

  <!-- mutations (sync)  -->
  <button @mousedown="store.commit('increment')">inc</button>
  <button @mousedown="store.commit('decrement')">dec</button>
  <button @mousedown="store.commit('add', { amount : 12})">+12</button>
  <button @mousedown="store.commit({ type : 'sub', amount : 12})">-12</button>

  <!-- actions (async)  -->
  <button @mousedown="store.dispatch('increment')">inc (async)</button>

  <!-- computed from 'vue' -->
  <div> {{ randomCount }} </div>

  
</template>

<script>

  import { useStore } from 'vuex'
  import { computed } from 'vue'

  export default {
    
    setup() {

      const store       = useStore()
      const randomCount = computed(() => store.state.count + Math.random())

      return { store, randomCount }

    }
    
  }

</script>

순살.


< 최종 정리 >

/* store.js */

import { createStore } from 'vuex'

const store = createStore({ 
    
    state       : {}, 
    mutations   : {}, 
    getters     : {}, 
    actions     : {}

})

export default store
<!-- App.vue -->

<script>

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {

  setup () {
  	
    // access a store
    const store = useStore() 

    return {
    
      // access a state in computed function
      count: computed(() => store.state.count),

      // access a getter in computed function
      double: computed(() => store.getters.double),
      
      // access a mutation
      increment: () => store.commit('increment'),

      // access an action
      asyncIncrement: () => store.dispatch('asyncIncrement')
      
    }
  }
}

</script>

< Strict Mode >

디버깅을 원할하게 하기위해 Strict Mode 를 지원한다.

const store = createStore({

  strict: true
  
})

위와 같이 strict 이 true 가 되면,

Vuex state 가 mutation 함수가 아닌 다른것에 의해 변경되었을때, error 를 던진다고한다.

주의할점은 strict mode 는 매우 비싸기때문에, 개발할때만 사용해야하며,

build 할때는 이 옵션을 반드시 꺼야한다. 


< 의문점 : 그럼 모든 컴포넌트 의 stateVuex 로? >

다행히, 이 질문에 대한 대답이 공식문서에 나와있다.

이중, 가장 중요한 부분은...

If a piece of state strictly belongs to a single component,
it could be just fine leaving it as local state.

만약 해당 컴포넌트의 데이터가 다른곳에서 절대 쓰일것이 아니라면,

local state 로 가지고있는것이 맞다.