Vuex란?

사용법 부터 정의까지 전부 정리해보았다.
윤여찬's avatar
Oct 04, 2023
Vuex란?
Contents
Module
vuex란 중앙관리소라고 생각하면 나한테는 이해가 빨랐다.
→ 데이터를 컴포넌트 마다 사용하게 된다면 중앙에서 데이터를 관리하여 더욱 사용성이 편리하게 하는것 동기와 비동기도 구분하여 사용할 수 있다. vue3의 pinia와 다른 점은 분명 존재하지만 한번 제대로 다시 복기해보자.
notion image
내가 기존 멍청하게 사용하던 방식(모든 컴포넌트에서 사용하는 데이터마다 다 store에 state에 때려박기…🤦‍♂️)이 아닌 props와 emit을 이용한 양방향으로 데이터를 주고 받는 방식은 한계가 존재한다.
컴포넌트의 개수가 많아지면 데이터 전달이 어려워 진다는 점이다…
해결 방식으로는 Event Bus와 Vuex가 존재하는데 나는 사실 Event Bus도 사실은 안써봣던 개발자였다… 이것은 바로잡고 넘어가려한다.

vuex 란?

  • vue.js에서의 상태 관리 라이브러리
  • vue.js 애플리케이션의 모든 컴포넌트에 대한 중앙 집중식 저장소
  • 다른 상태 관리 라이브러리 Flux(facebook), Redux(React)에서 영감을 받음
    • vue.js에서도 Redux를 사용할 수 있지만, Vuex와의 호환이 좋고 더 직관적으로 개발 가능

state

vue의 data와 같다.
→ mutation을 통해서만 변경이 가능하다.
  • mutation을 통해 state가 변경이 일어나면 반응적으로 View가 업데이트

mutations

state를 변경하는 유일한 방법이다.
일반적으로는 commit을 통해서만 호출이 가능하다.
  • Helper 함수로 직접 호출 가능.
첫 번째 인자는 state, 두 번째 인자는 payload이다.

actions

비동기 작업이 가능함
dispatch를 이용해서 호출한다.
mutation을 호출하기 위한 commit이 가능하다.
  • action에서도 mutation을 통해 state 변경을 가능하게 할 수 있다.
첫 번째 인자는 state, 두 번째 인자는 payload이다.

getters

vue의 computed와 같다.(계산된 속성)
  • getter의 결과는 종속성에 따라 캐시 되고 변경된 경우에만 다시 계산한다.
state에 대해 연산을 하고 그 결과를 view에 반인딩 할 수 있다.
state의 변경 여부에 따라 다시 계산하고 view를 업데이트
→ 내가 기억하기론 state에 바로 접근하는건 굉장히 나쁜 행동으로 getters를 걸쳐 접근하라고 배웠다. 그렇기에 나는 getters로 계속 접근했던 것으로 기억한다.

modules

모듈별로 stare를 분리하고 관리가 가능했다.
실무에서는 store를 하나만 사용하기엔 힘든 상황이 많다 그렇기에 필요함.
→ 모듈화
내가 가장 몰랏고 그냥 넘어갔던 이 부분 모듈화를 뜯어보자…
사용하는 방법은 store에 modules 속성에 직접 모듈을 추가한다.
modules라는 폴더 하위에 각 단위별로 분리한 모듈 파일 import하여 추가 가능
namespace
→ 복잡한 앱을 개발할 때 getters & mutations & actions의 이름을 유일하게 정하지 않으면 충돌이 발생한다. namespace를 통해서 충돌을 방지한다.
→ 이 부분은 따로 더 자세히 작성이 필요하다.

vuex 한글 문서

조금 더 깊게 분석해보자…

vuex 기본 구조

store
component
script내 등록 위치
동작/변경 방법
$store 호출방식
비고
state
...mapState
computed
this.$store.state
저장된 상태
getters
...mapGetters
computed
this.$store.getters
state를 화면에 바인딩 할 수 있음
actions
...mapMutation
methods
commit(state, payload)
this.$store.commit()
- UPPER_CASE사용 - state 값을 변경하는 역할 - 동기적 (sync)
mutation
...mapAction
methods
dispatch(state, payload)
this.$store.dispatch()
- http 통신 등 언제 결과를 받아올 지 예측할 수 없을 경우 사용 - state 내용을 render함 - 비동기적 (async)

vuex 흐름

  1. store 생성
const store = new Vuex.Store({ state: { count: 0, }, getters: {}, actions: {}, mutations: {}, })
  1. vuex를 vue 컴포넌트에 가져오기
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
  1. getters
vuex는 gettersstore 안에 정의하는것을 허락합니다.
getters 는 저장소 상태(state)의 값을 기반으로 상태를 계산해야 할 때 사용한다.
→ state의 데이터들을 특정 로직을 이용해서 빠르게 가져올 수 있다.
4. map Helper
헬퍼 함수에는 mapState(), mapGetters(), mapMutations(), mapActions() 가 있다.
mapState - state를 연결해주는 함수
mapGetters - getters를 연결해주는 함수
mapMutations - mutations를 연결해주는 함수
mapActions - actions를 연결해주는 함수
computed: { ...mapState({ cnt: 'count', }), ...mapGetters({ count: 'count'., }), }
state는 무조건 computed에 선언해주어야하지만 getters는 computed에 선언하지 않아도 되지만 권장하는 방법은 computed에 선언하는 것으로 권장하고있다.
methods: { ...mapMutations({ a: 'a', b: 'b' }), ...mapActions({ c: 'c' }), },
mutations와 actions는 methods에 선언해준다.

Module

여러개의 저장소()를 모듈로 나눌 수 있다.
각 module은 자체 state, mutation, action, getter및 모듈을 중첩하여 포함할 수도 있다.
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) store.state.a store.state.b

네임스페이스, namespaced

기본적으로 module 내의 actions, mutations, getter는 전역 네임 스페이스로 등록된다.
모듈이 독립적이거나 재사용되길 원하는 경우 namespaced: true 를 설정해주면된다.
만약에 큰 프로젝트를 작업할 경우 네임스페이스를 사용하는 것이 좋다고 한다.
const store = new Vuex.Store({ modules: { ... namespaced: true } })
우리 회사에서는 어떻게 사용하고 있는지 확인해보니
import {createNamespacedHelpers} from 'vuex'; const {mapState, mapMutations} = createNamespacedHelpers('message'); export default { name: 'Message', computed: mapState([ 'isShow', 'message', 'buttons', ]), watch: { ... }, methods: { ...mapMutations([ 'hide', ]), ... }
위 처럼 createNamespacedHelpers를 사용해서 가져온 후 map helper를 사용해서 응용하고있었다.

vuex의 component에 store를 바인딩하는 방법

전 회사에서는 module을 나눠 사용하지 않았지만 큰 프로젝트를 만들때는 vuex를 사용할 때 store를 module 별로 분리하는 것이 바람직하다고 한다.
그렇지 않으면 바인딩 된 store의 state들이 다른 컴포넌트의 루틴에 오염될 가능성과 코드의 복잡성이 높아지게 된다고 한다…
 
공식 홈페이지에서 명시된 기본 store 바인딩 방법
import { mapState } from 'vuex'; new Vue({ computed: mapState({ count: state => state.count, countAlias: 'count' }) })
mapState라는 helper 함수를 사용해 객체 형태로 count를 바인딩 한 형태이다. 이때 아래처럼 state의 이름을 그대로 상속 받아 정의할 수 있다.
import { mapState } from 'vuex'; new Vue({ computed: mapState([ 'count' ]) })
기본적인 바인딩 방법은 가장 단순한 구조를 가진 Store를 바인딩하였을 때이다. 하지만 Store가 여러 모듈별로 분리 되어 있고 하나의 컴포넌트에서는 여러 Store 모듈을 바인딩해야 한다면 매우 복잡해질 것 이라한다.
 
좋은 예시를 보여주는 블로그를 찾았다.
 
각각 User, Book 이라는 store 모듈이 존재한다고 가정하자.
└─ A └─ B ├─ User └─ C └─ Book
의 경로 처럼 User는 A/B 경로에 있으며, Book은 A/B/C 경로에 위치한다면 ?
import {mapState, mapActions} from 'vuex' new Vue({ computed: { ... mapState({ bookList: state => state.A.B.C.Book.list }) }, methods: { ...mapActions([ 'A/B/C/Book/setList' ]) } })
위 처럼 bookList를 불러오기 위해선 Book에 접근을 위해 state.A.B.C.Book으로 접근해야 list에 접근이 가능하다.
bookList: state => state.A.B.C.Book.list
확실히 가독성이 많이 부족해보인다.
 
이 때 createNamespacedHelpers를 사용하면 아래처럼 할 수 있다.
import {createNamespacedHelpers} from 'vuex' const {mapState} = createNamespacedHelpers('A/B/C/Book') new Vue({ computed: { ... mapState({ bookList: state => state.list }) } })
위에서 createNamespacedHelpers 를 사용해서 정의를 해주고 나니 확실히 아래 computed에서 사용할 때 가독성이 좋아진 것 처럼 보인다.
 
우리 회사에서 사용한 방법을 확인해보니 아래와 같았다.
const { mapGetters: widgetGetters, mapActions: widgetActions } = createNamespacedHelpers('api/dashboard/widget'); const { mapGetters: stationGetters, mapActions: stationActions } = createNamespacedHelpers('api/station/list');
위 처럼 경로를 맞추고 mapGetters와 mapActions를 가져와 각각의 이름을 가독성과 헷갈리지 않도록 리네임 해주었다.
이유는 위에 mapGetters와 mapActions가 두 번씩 쓰이는데 이런 경우 나중에 문제가 생길 수 있다. 그렇기에 이름을 지정해줘서 혼동을 줄이고 가독성을 높일 수 있었다.
 
위 블로그에도 우리 회사에서 사용하는 방식과 같은 설명을 해주셨다.
import {createNamespacedHelpers} from 'vuex' const {mapState: userMapState, mapActions: userMapActions} = createNamespacedHelpers('A/B/User'), {mapState: bookMapState, mapActions: bookMapActions} = createNamespacedHelpers('A/B/C/Book') new Vue({ computed: { ... userMapState({ userList: state => state.user.userList }), ... bookMapState([ 'list' ]) }, methods: { ...userMapActions({ setUserList: 'setUserList' }), ...bookMapActions([ 'setBookList' ]) } })
여러번 다른 모듈을 가져와서 사용하는데 해당 방법을 사용해서 사용했는데 아래처럼 더 효과적으로 정의할 수 있다고한다.
import {createNamespacedHelpers} from 'vuex' const userListHelper = createNamespacedHelpers('A/B/User'), bookListHelper = createNamespacedHelpers('A/B/C/Book') new Vue({ computed: { ... userListHelper.mapState({ userList: state => state.user.userList }), ... bookListHelper.mapState([ 'list' ]) }, methods: { ...userListHelper.mapActions({ setUserList: 'setUserList' }), ...bookListHelper.mapActions([ 'setBookList' ]) } })
이렇게 사용하면 userListHelper 에 접근하여 .mapState 로 바로 userList에 접근이 가능해보인다. 확실히 이렇게 사용하면 코드의 가독성도 높고 바인딩에 헷갈리는 일이 없어보였다.

정리

→ 아래의 사진이 진짜 정확했다.
notion image
State: 상태 말 그대로 상태를 나타냄 다른 거 없음 그냥 그대로 상태
Getters: 여러 컴포넌트에서 vuex state를 각자 참조해서 사용하면 효율이 떨어짐 그렇기에 state에 기반한 별도의 값을 만들어서 getters 내에 속성으로 저장(캐쉬) 해두고 getters 내에 저장된 속성을 참조해서 사용하게 된다.
Mutations: 동기 역할을 한다.
Actions: 비동기 역할을 한다.
 
→ 내가 제일 부족했던 module 부분 아마도 쓰게 되면 또 헷갈리겠지만 … 제대로 확인하자 vue2, vue3 다 제대로해야하니깐
 
Share article

찬찬잉