[vue.js] Vuex 정리

Yeonjoo Yoo·2022년 2월 6일
3

TIL

목록 보기
9/12
post-custom-banner
vue에서 상태 관리를 하기 위한 라이브러리인 vuex에 대해 정리하고자 한다.

One-Way Data Binding in Vue

  • Vue.js에서 부모-자식 컴포넌트 사이에 단방향 바인딩을 형성
  • 부모는 props를 통해 자식에게 데이터를 전달하고, 자식은 events(emit)를 통해 부모에게 전달
    • props는 아래로, events 위로 전달
  • 하위 컴포넌트가 실수로 상위 컴포넌트의 상태를 변경하여 앱의 데이터 흐름을 추론하기 더 어렵게 만드는 것을 방지하기 위함
  • 컴포넌트의 개수가 많아지면 컴포넌트 간에 데이터 전달이 어려워짐
    만약, 한 컴포넌트에서 데이터를 수정할 때마다 prop, emit을 사용해 전달해야하는데 그 과정이 매우 복잡해질 수 있음
    • 해결 방법으로 Event Bus와 Vuex 등이 있음
  • "단반향 흐름" 컨셉의 간단히 묘사한 그림 (Vue 인스턴스 내 상태 관리)
    1. state : 컴포넌트 간에 공유할 data
    2. view : 데이터가 표현될 template
    3. actions : 사용자의 입력에 따라 반응할 methods
    • 이런 단순한 상황은 여러개의 컴포넌트가 같은 state를 공유하는 경우에 빠르게 무너짐
new Vue({
  // state
  data() {
    return {
      counter: 0
    };
  },
  // view
  template: `
  <div>{{ counter }}</div>
  <button @click="increment">PLUS</button>
  `,
  // actions
  methods: {
    increment() {
      this.counter++;
    }
  }
});

v-model을 사용하여 양방향 바인딩 가능 (two-way binding)

상태 관리 (State Management)

상태 (state) 란?

  • 어떤 객체가 가진 데이터 (data)
    • 객체 간 데이터를 주고 받으며 application이 구현됨
    • 예를 들어, Vue에서는 컴포넌트(객체) 에 data가 상태라 할 수 있음
  • 지역적(local) 또는 전역적(global) 상태를 적절히 구분해서 설계해야 함
    • 지역적으로 props와 emit으로 컴포넌트 내 데이터를 전달
    • 전역적으로 vuex 스토어에 저장하던지, 브라우저의 쿠키(Cookie) 또는 로컬 스토리지(localStorage)에 저장하여 데이터 공유 가능

상태 관리가 필요한 이유

  • 규모가 크고 복잡한 application에서 발생할 수 있는 컴포넌트 간 통신 및 데이터 전달 문제를 해결 가능
  • 규모가 크고 복잡한 앱일수록
    1. props, emit 이 거쳐야 할 컴포넌트가 많아짐
    2. Event Bus를 쓰면 컴포넌트 간 데이터 흐름을 파악하기 어려움
  • 이러한 문제점을 해결하기 위해 모든 데이터 통신을 한 곳에서 중앙 집중식으로 관리하는 것이 상태 관리
  • 규모가 작은 프로젝트에서는 필요 없을 수 있음

Event Bus 문제점
컴포넌트가 많아지면 어디서 어디로 보냈는지 관리가 되지 않음
(컴포넌트 간 데이터 흐름을 파악하기 어려움)

Vuex 소개

Vuex 란?

  • Vue.js에서의 상태 관리 라이브러리
  • Vue.js 애플리케이션의 모든 컴포넌트에 대한 중앙 집중식 저장소 역할
  • 다른 상태 관리 라이브러리인 Flux(facebook), Redux (React)에서 영감을 받음
    • Vue.js에서도 Redux를 사용할 수 있지만, Vuex와의 호환이 좋고 더 직관적으로 개발 가능
    • Vue.js의 반응성(Reactivity) 체계를 효율적으로 활용하여 화면을 업데이트 가능

Vue 버전에 맞는 Vuex 버전 확인

  • Vue 2 & Vuex 3
  • Vue 3 & Vuex 4

특징

Single Source of Truth

  • 하나의 어플리케이션은 하나의 store만 가진다는 것을 의미
    • 하나의 객체가 어플리케이션의 전체 state를 포함하고, 이 객체가 하나의 원본 소스 임
  • 단일 상태 트리는 특정 state를 바로 찾을 수 있게 만들고, 현재 앱의 state의 스냅샷을 가져올 수 있어 디버깅을 간편하게 도움

state is Reactive

  • 상태가 변경되면 이 상태를 공유하는 다른 곳에서도 상태가 업데이트 됨

state management pattern + library

  • Vuex는 상태 관리 패턴이자 라이브러리
  • 여러 컴포넌트 간의 데이터 전달과 이벤트 통신을 한곳에서 관리하는 패턴을 Vuex를 통해 구현 가능
  • 상태 관리 및 특정 규칙 적용과 관련된 개념을 정의하고 분리함으로써, 코드의 구조와 유지 관리 기능 향상

주의 사항

  • 페이지 새로고침시 store에 데이터가 사라짐
    이 문제는 브라우저 쿠키 or 스토리지나 다른 라이브러리 등으로 처리 가능
    • vuex-persistedstate 라이브러리
    • 모든 store 값들을 localstorage로 저장할 시 속도 이슈가 발생할 수 있음
  • 공통적으로 사용하는 상태가 아닌 경우는 메모리를 낭비할 수 있음
  • Vuex 저장소가 일반 전역 개체와 다른 두 가지
  1. Vuex store는 반응형
    Vue 컴포넌트는 상태를 검색할 때 저장소의 상태가 변경되면 효율적으로 대응하고 업데이트 함
  2. 상태를 직접 변경할 수 없음
    저장소의 상태를 변경하는 유일한 방법은 명시적인 commit을 이용한 변이
    이렇게하면 모든 상태에 대한 추적이 가능한 기록이 남을 수 있으며 툴을 사용하여 앱을 더 잘 이해할 수 있음

Vuex 구조 (Core Concept)

  • 관리 포인트는 store 패턴을 사용

state

  • Vue의 data와 같음
  • 원본 소스의 역할을 하며, View와 직접적으로 연결되어있는 Model
  • state는 mutation을 통해서만 변경이 가능
    • mutation을 통해 state가 변경이 일어나면 반응적으로 View가 업데이트

mutations

  • state를 변경하는 유일한 방법
  • 일반적으로 commit을 통해서만 호출 할 수 있음
    • Helper 함수로 직접 호출 가능
  • 첫 번째 인자는 state, 두 번째 인자는 payload
store.commit({
  type: 'increment',
  amount: 10
})
/*-----------------------------*/
// store.js
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
  • 주로 API를 통해 전달받은 데이터를 mutations에서 가공하여 state를 설정하는데 사용됨

actions

  • 비동기 작업이 가능
    • 그 외에는 mutation과 비슷
  • mutation을 호출하기 위한 commit이 가능
    • action에서도 mutation을 통해 state를 변경 가능
  • dispatch를 통해 호출할 수 있음
  • 첫 번째 인자는 context, 두 번째 인자는 payload
    • context는 state, commit, dispatch, rootstate 속성들을 포함
store.dispatch('incrementAsync', {
  amount: 10
})
// or
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
/*-----------------------------*/
// store.js
actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}
  • 주로 axios를 통한 api 호출과 그 결과에 대해 return을 하거나 mutation으로 commit하는 용도로 사용

getters

  • Vue의 computed와 같음 (계산된 속성)
    • getter의 결과는 종속성에 따라 캐시 되고 변경된 경우에만 다시 계산
  • state에 대해 연산을 하고 그 결과를 view에 바인딩 할 수 있음
  • state의 변경 여부에 따라 다시 계산하고 view를 업데이트

modules

  • 모듈별(기능 또는 페이지)로 store를 분리하고 관리 가능
  • 실무에서는 단 하나의 store를 사용하기 힘듦
  • 모듈화에서 추가 내용 참고

Vuex 전체 흐름도

  • Vuex 또한 단방향 데이터 흐름을 가짐
  • 실행 순서
    1. Vue Component에서 'dispatch([actions method name])'를 통해 action을 실행
      (예를 들어, 버튼 클릭 등 이벤트 발생)
    2. action에서는 외부 Api를 호출하는 등 비동기 로직을 처리
    3. 비동기 결과를 'commit([mutations method name])'를 통해 mutation을 실행
    4. mutation에서 state를 변경
    5. getter를 이용하여 다시 Vue Compoent에 바인딩되어 화면 갱신

사용 방법

설치 (NPM)

  1. vuex install : npm install vuex
  2. Vue.use() 통해 Vuex 명시적으로 추가
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

<script> 태그 사용 시, 해당 작업 필요 없음

  1. Promise install : npm install es6-promise
    • Vuex는 Promise 를 필요로 함
    • 브라우저에서 Promise를 구현하지 않은 경우에만 설치하기
    • Vuex 사용 전에 import 'es6-promise/auto' 를 아무 곳에나 추가하기

예제

  • 폴더 구조
├── store
│   └── index.js
├── App.vue
└── main.js
  1. App.vue
<template>
  <div id="app">
    <div>
      <label>{{ storeGetMsg }}</label>
      <input type="number" v-model.number="num"/>
      <button @click="onChangedMsg">PLUS</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      num: 1
    }
  },
  computed: {
    storeGetMsg () {
      return this.$store.getters.getMsg
    }
  },
  methods: {
    onChangedMsg () {
      this.$store.dispatch('callMutation', { num: this.num })
    }
  }
}
</script>
  1. store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num: 0
  },
  mutations: {
    changeNumber (state, val) {
      state.num += val
    }

  },
  actions: {
    callMutation ({ state, commit }, { num }) {
      commit('changeNumber', num)
    }
  },
  getters: {
    getMsg (state) {
      return `calculated => ${state.num}`
    }
  }
})
  1. main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store/index'

Vue.config.productionTip = false

new Vue({
  store: store,
  render: h => h(App),
}).$mount('#app')

Vuex Helper 함수

  • Vuex 기술 요소들을 컴포넌트에서 더 편하게 쓸 수 있도록 도와주는 API
    • "this.$store.(속성명)"을 사용하지 않고 접근 가능하도록 도움
    • "this"로 접근 가능
  • Spread Operator (...)와 함께 사용 (여러 개의 객체를 하나로 통합)

종류

  • state → mapState
  • getters → mapGetters
  • mutations → mapMutations
  • actions → mapActions

예제

<template>
  <div>
    <!-- <p>{{ $store.state.num }}</p> -->
    <p>{{ num }}</p>
    <button @click="clickBtn">popup message</button>
    <button @click="getPopup">show popup</button>
  </div>
</template>
<script>
  // App.vue
  import { mapState, mapGetters, mapMutations, mapAcations } from 'vuex'

  export default {
    // this.$store.state.num == mapState(['num'])
    // this.$store.getters.countedNum == mapGetters(['countedNum'])
    computed() {
  	  ...mapState(['num']),
  	  ...mapGetters(['countedNum']) 
  	},
    methods: {
  	  ...mapMutations(['clickBtn', 'addNumber']),
      ...mapActions({
        getPopup: 'showPopup' // 컴포넌트에서 쓸 명칭 : store의 action 명
  	  }) 
 	}
  }
</script>

Store 모듈화

사용 방법

  • Store에 modules 속성에 직접 모듈 추가
  • modules 라는 폴더 하위에 각 단위별로 분리한 모듈 파일 import하여 추가 가능

namespace

  • 복잡한 앱을 개발할 때 getters & mutations & actions 의 이름을 유일하게 정하지 않으면 충돌 발생할 수 있음
  • namespace 를 통해 충돌 방지 가능
  1. 네임스페이스를 구분하기 위해 types.js 로 각 속성의 이름들을 빼고 store.js 와 각 컴포넌트에 import 하여 사용하는 방법이 있음
  2. "namespaced: true" 로 설정하여 namespace 추가 가능
    해당 모듈에 접근할 때 namespace 붙이기!

예제

import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

import coffee from "./modules/groups";

export default new Vuex.Store({
  state: () => {},
  getters: {},
  actions: {},
  mutations: {},
  modules: {
    groups: groups,
    guest: {
      actions: {
        login () { ... } // -> dispatch('login')
      }
    },
    account: {
      namespaced: true,

      // module assets
      state: () => ({ ... }), // module state is already nested and not affected by namespace option
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // nested modules
      modules: {
        // inherits the namespace from parent module
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // further nest the namespace
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

정리

  • Vuex는 Vue.js에서 사용하는 상태 관리 라이브러리
  • 모든 데이터 통신을 한 곳에서 중앙 집중식으로 관리하는 패턴
  • 모든 컴포넌트에서 state에 접근하거나 action을 실행할 수 있음
  • 단순한 App에서 Vuex를 무작정 사용하는 것 보다 props&emit 또는 Event Bus를 사용하는 것이 좋음
    그러나, 중대형 규모의 SPA를 구축하는 경우 Vuex를 통해 Vue컴포넌트 외부의 상태를 보다 잘 처리할 수 있음

참고
https://vuex.vuejs.org
https://vuejs.org/v2/guide/components.html
https://vuejs.org/v2/api/#v-model
https://bbosong-develop.tistory.com/3
https://kdydesign.github.io/2019/05/09/vuex-tutorial
https://dev-jsk.tistory.com/77

profile
to be frontend engineer
post-custom-banner

0개의 댓글