공부방

Vuex 본문

vue.js

Vuex

코딩 화이팅 2023. 5. 11. 14:18
  • Vue.js 애플리케이션에 대한 상태관리패턴+라이브러리
  • 애플리케이션 모든 컴포넌트들의 중앙 저장소 역할(데이터 관리)
  • 부모 자식 단계가 많이 복잡해진다면 데이터의 전달하는 부분이 매우 복잡해짐.
  • 애플리케이션이 여러 구성 요소로 구성되고 더 커지는 경우 데이터를 공유하는 문제 발생

비 부모-자식간 통신

  • 두 컴포넌트가 통신할 필요는 있지만 서로 부모/자식이 아닐수도 있다.
  • 비어 있는 Vue Instance 객체를 Event Bus로 사용

상태 관리 패턴

  • 상태는 앱을 작동하는 원본 소스(데이터)
  • 는 상태의 선언적 매핑입니다(상태를 보여주는 화면)
  • 액션은 뷰에서 사용자 입력에 대해 반응적으로 상태를 바꾸는 방법(메소드)

Vuex 핵심 컨셉

Vuex 저장소 개념

  • State : 단일 상태 트리 사용(데이터를 저장하는 곳), 애플리케이션마다 하나의 저장소를 관리(data)
  • Getters : Vue Instance의 computed와 같은 역할, State를 기반으로 계산(computed)
  • Mutations : State의 상태를 변경하는 유일한 방법(methods)
  • Actions : 상태를 변이시키는 대신 액션으로 변이(Mutations)에 대한 커밋 처리(비동기 methods)
  • module

State

  • Vuex는 단일 상태 트리 사용
  • 이 단일 객체는 모든 애플리케이션 수준의 상태를 포함하며 "원본 소스"의 역할
  • 각 애플리케이션마다 하나의 저장소만 갖게 된다는 것을 의미
  • 애플리케이션에서 공유해야 할 data 관리
  • State에 접근 방식 : this.$store.state.데이터 이름
  • computed를 사용하여 데이터를 가져와 사용 가능(값이 변경되면 해당 state를 공유하는 여러 컴포넌트의 DOM은 알아서 렌더링)
  • 모든 상태를 Vuex에서 관리해야 하는 것은 아님
  • Vuex에 저장하면 코드가 장황하고 간접적으로 변할수도 있다.

Getters

  • State를 변경하지 않고 활용하여 계산을 수행(computed 속성과 유사)
  • 실제 계산된 값을 사용하는 것처럼 getters는 저장소의 상태를 기준으로 계산
  • computed 속성과 마찬가지로 state 종속성에 따라 캐시되고, 일부 종속성이 변경된 경우에만 다시 재계산
  • getters 자체가 state를 변경하지는 않는다.

Mutations

  • Vuex 저장소에서 실제로 상태를 변경하는 유일한 방법
  • 각 컴포넌트에서 State의 값을 직접 변경하는 것은 권하지 않음
  • mutation의 핸들러 함수는 반드시 동기적이어야 함.
    (비동기 콜백함수의 실제로 호출 시기를 알 수 있는 방법이 없음. 추적x)
  • 첫번째 인자로 항상 state를 받음
  • Mutations는 직접 호출이 불가능, store.commit('정의된 이름(메소드 이름)')으로 호출
  • Actions에서 commit() 메서드에 의해 호출

Actions

  • state를 변이시키는 대신 commit() 메서드를 통해 mutations호출
  • 비동기 작업의 결과를 적용하려고 할 때 사용.(Backend API와 통신 등)
  • context 객체 인자를 받음
    (store.index.js 파일 내에 있는 모든 요소의 속성 접근 & 메서드 호출 가능)
    (state를 직접 변경할 수 있지만 하지 말기 : 명확한 역할 분담을 하여 올바르게 상태 관리)
  • 컴포넌트에서 dispatch() 메서드에 의해 호출

Vuex 언제 사용?

  • Vuex는 공유된 상태 관리를 처리하는데 유용하지만, 개념에 대한 이해와 시작하는 비용도 함께 발생
  • 앱이 단순하다면 Vuex 없이도 괜찮다.(간단한 글로벌 이벤트 버스 OK)
  • 중대형 규모의 SPA를 구축하는 경우

Vuex 설치

  • CDN  방식
    <script src="/path/to/vue.js"></script>
    <script src="/path/to/vuex.js"></script>
  • NPM 방식
    npm install vuex --save
  • Vue CLI
    vue add vuex
    (프로젝트를 진행하던 중에 추가를 하게 되면 App.vue를 덮으쓰므로 백업을 해두고 추가할 것)
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
================================================================
store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    //공통의 데이터(상태)들을 저장하는 영역
  },
  getters: {
    //state를 이용하여 원본의 데이터를 수정하지 않은 상태로 새로운 값을 뿌려주고 싶을때
    //computed와 같은 역할을 하고 있음.
  },
  mutations: {
    //state를 변경하는 유일한 방법 , 첫번째 인자로 state가 들어옴
    //동기적으로 작성할 것!!
  },
  actions: {
    //mutations를 호출
    //backend api와 통신을 하는곳, context라고 하는 만능 객체가 인자로 들어옴
  },
  modules: {
    //여러개로 쪼개놓고 관리를 하는 곳
  },
});

컴포넌트 형태로 클릭하면 각자의 갯수와 전체 갯수 출력하기

components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
    <h3>{{ countMsg }}</h3>
  </div>
</template>

<script>

export default {
  name: "ResultView",
  props: {
    total: Number,
  },
};
</script>

<style></style>
===============================================================
components/SubjectView.vue
<template>
  <div>
    <button @click="addCount">{{ title }}-{{ count }}</button>
  </div>
</template>

<script>
export default {
  name: "SubjectView",
  props: {
    title: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addCount() {
      this.count += 1;
      this.$emit("add-to-count");
    },
  },
};
</script>

<style></style>
===============================================================
App.vue
<template>
  <div id="app">
    <h2>당신이 좋아하는 파트를 선택하세요.</h2>
    <!-- 사이트의 위에 버튼을 만들어주기 -->
    <result-view :total="total"></result-view>
    <subject-view @add-to-count="addTotalCount" title="코딩"></subject-view>
    <subject-view @add-to-count="addTotalCount" title="알고"></subject-view>
  </div>
</template>

<!-- vue를 생성하면 바로 import해주기 -->
<script>
import ResultView from "./components/ResultView.vue";
import SubjectView from "./components/SubjectView.vue";

// 이름을 정해주고
// import한 거 컴포넌트에 등록해주기
export default {
  name: "App",
  components: {
    ResultView,
    SubjectView,
  },
  data() {
    return {
      total: 0,
    };
  },
  methods: {
    addTotalCount() {
      this.total += 1;
    },
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

State사용

store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    //공통의 데이터(상태)들을 저장하는 영역
    total: 0,
  },
  getters: {
    //state를 이용하여 원본의 데이터를 수정하지 않은 상태로 새로운 값을 뿌려주고 싶을때
    //computed와 같은 역할을 하고 있음.
  },
  mutations: {
    //state를 변경하는 유일한 방법 , 첫번째 인자로 state가 들어옴
    //동기적으로 작성할 것!!
  },
  actions: {
    //mutations를 호출
    //backend api와 통신을 하는곳, context라고 하는 만능 객체가 인자로 들어옴
  },
  modules: {
    //여러개로 쪼개놓고 관리를 하는 곳
  },
});
===============================================================
components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
  </div>
</template>

<script>
export default {
  name: "ResultView",
  computed: {
    total() {
      return this.$store.state.total;
    },
  },
};
</script>

<style></style>
===============================================================
components/SubjectView.vue
<template>
  <div>
    <button @click="addCount">{{ title }} - {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: "SubjectView",
  props: {
    title: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addCount() {
      this.count += 1;
      //view 단에서 냅다 state에 접근을 하고 있는데
      //이거 가넝하지만 하라고 말라고? --> 말라고~~ 연습이니까~~
      this.$store.state.total++;
    },
  },
};
</script>

<style></style>
===============================================================
<template>
  <div id="app">
    <h2>당신이 좋아하는 파트를 선택하세요.</h2>
    <result-view></result-view>
    <subject-view title="코딩"></subject-view>
    <subject-view title="알고"></subject-view>
  </div>
</template>

<script>
import ResultView from "./components/ResultView.vue";
import SubjectView from "./components/SubjectView.vue";

export default {
  name: "App",
  components: {
    ResultView,
    SubjectView,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Getters

store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    //공통의 데이터(상태)들을 저장하는 영역
    total: 0,
  },
  getters: {
    //state를 이용하여 원본의 데이터를 수정하지 않은 상태로 새로운 값을 뿌려주고 싶을때
    //computed와 같은 역할을 하고 있음.
    //현재 total의 투표수에 따라 문구를 달리 출력하고 싶다.~~~
    countMsg(state) {
      let msg = "10회 ";
      if (state.total > 10) msg += "초과";
      else msg += "이하";

      return `${msg} 호출됨`;
    },
  },
  mutations: {
    //state를 변경하는 유일한 방법 , 첫번째 인자로 state가 들어옴
    //동기적으로 작성할 것!!
  },
  actions: {
    //mutations를 호출
    //backend api와 통신을 하는곳, context라고 하는 만능 객체가 인자로 들어옴
  },
  modules: {
    //여러개로 쪼개놓고 관리를 하는 곳
  },
});
================================================================
components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
    <h3>{{ countMsg }}</h3>
  </div>
</template>

<script>
export default {
  name: "ResultView",
  computed: {
    total() {
      return this.$store.state.total;
    },
    countMsg: function () {
      return this.$store.getters.countMsg;
    },
    // countMsg(){
    //   return this.$store.getters.countMsg
    // }
  },
};
</script>

<style></style>
================================================================
components/SubjectView.vue
<template>
  <div>
    <button @click="addCount">{{ title }} - {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: "SubjectView",
  props: {
    title: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addCount() {
      this.count += 1;
      //view 단에서 냅다 state에 접근을 하고 있는데
      //이거 가넝하지만 하라고 말라고? --> 말라고~~ 연습이니까~~
      this.$store.state.total++;
    },
  },
};
</script>

<style></style>
================================================================
App.vue
<template>
  <div id="app">
    <h2>당신이 좋아하는 파트를 선택하세요.</h2>
    <result-view></result-view>
    <subject-view title="코딩"></subject-view>
    <subject-view title="알고"></subject-view>
  </div>
</template>

<script>
import ResultView from "./components/ResultView.vue";
import SubjectView from "./components/SubjectView.vue";

export default {
  name: "App",
  components: {
    ResultView,
    SubjectView,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

헬퍼를 사용한 mapgetters

나머지 코드는 위와 동일
components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
    <h3>{{ countMsg }}</h3>
  </div>
</template>

<script>
//헬퍼를 이용하면 굉장히 편리해진다.
import { mapGetters } from "vuex";

export default {
  name: "ResultView",
  computed: {
    //물론 state도 가넝하다.
    //mapState라는걸 가져오면 (ㅎㅎ 직접 해볼것)
    total() {
      return this.$store.state.total;
    },
    ...mapGetters(["countMsg"]),
  },
};
</script>

<style></style>

동작도 위와 동일

mutations

store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    //공통의 데이터(상태)들을 저장하는 영역
    total: 0,
  },
  getters: {
    //state를 이용하여 원본의 데이터를 수정하지 않은 상태로 새로운 값을 뿌려주고 싶을때
    //computed와 같은 역할을 하고 있음.
    //현재 total의 투표수에 따라 문구를 달리 출력하고 싶다.~~~
    countMsg(state) {
      let msg = "10회 ";
      if (state.total > 10) msg += "초과";
      else msg += "이하";

      return `${msg} 호출됨`;
    },
  },
  mutations: {
    //state를 변경하는 유일한 방법 , 첫번째 인자로 state가 들어옴
    //동기적으로 작성할 것!!
    ADD_ONE(state) {
      state.total += 1;
    },
    //payload 숫자 값을 넘긴 상태이므로 바로 더할 수 있음
    ADD_TEN(state, payload) {
      state.total += payload;
    },
    //payload 객체가 들어왔다.
    ADD_RANDOM(state, payload) {
      state.total += payload.num;
    },
  },
  actions: {
    //mutations를 호출
    //backend api와 통신을 하는곳, context라고 하는 만능 객체가 인자로 들어옴
  },
  modules: {
    //여러개로 쪼개놓고 관리를 하는 곳
  },
});
==============================================================
components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
    <h3>{{ countMsg }}</h3>
  </div>
</template>

<script>
//헬퍼를 이용하면 굉장히 편리해진다.
import { mapGetters } from "vuex";

export default {
  name: "ResultView",
  computed: {
    //물론 state도 가넝하다.
    //mapState라는걸 가져오면 (ㅎㅎ 직접 해볼것)
    total() {
      return this.$store.state.total;
    },
    ...mapGetters(["countMsg"]),
  },
};
</script>

<style></style>
==============================================================
<template>
  <div>
    <button @click="addOneCount">{{ title }} + 1 - {{ count }}</button>
    <button @click="addTenCount">{{ title }} + 10 - {{ count }}</button>
    <button @click="addRandomCount">{{ title }} + ? - {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: "SubjectView",
  props: {
    title: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addOneCount() {
      this.count += 1;
      //mutations는 직접 부를순없고 메서드를 통해서 부를 수 있음.
      //commit('메서드이름'[,넘기고 싶은 인자])
      this.$store.commit("ADD_ONE");
    },
    addTenCount() {
      this.count += 10;
      //값을 인자로 넘겼다.
      this.$store.commit("ADD_TEN", 10);
    },
    addRandomCount() {
      let num = Math.round(Math.random() * 100);
      this.count += num;
      //객체를 인자로 넘겼다.
      this.$store.commit("ADD_RANDOM", { num });
    },
  },
};
</script>

<style></style>
==============================================================
App.vue
<template>
  <div id="app">
    <h2>당신이 좋아하는 파트를 선택하세요.</h2>
    <result-view></result-view>
    <subject-view title="코딩"></subject-view>
    <subject-view title="알고"></subject-view>
  </div>
</template>

<script>
import ResultView from "./components/ResultView.vue";
import SubjectView from "./components/SubjectView.vue";

export default {
  name: "App",
  components: {
    ResultView,
    SubjectView,  
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

첫번째 버튼을 누르면 1씩 증가 두번째 버튼을 누르면 10씩 증가 세번째 버튼을 누르면 랜덤으로 증가한다.

actions

store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    //공통의 데이터(상태)들을 저장하는 영역
    total: 0,
  },
  getters: {
    //state를 이용하여 원본의 데이터를 수정하지 않은 상태로 새로운 값을 뿌려주고 싶을때
    //computed와 같은 역할을 하고 있음.
    //현재 total의 투표수에 따라 문구를 달리 출력하고 싶다.~~~
    countMsg(state) {
      let msg = "10회 ";
      if (state.total > 10) msg += "초과";
      else msg += "이하";

      return `${msg} 호출됨`;
    },
  },
  mutations: {
    //state를 변경하는 유일한 방법 , 첫번째 인자로 state가 들어옴
    //동기적으로 작성할 것!!
    ADD_ONE(state) {
      state.total += 1;
    },
    //payload 숫자 값을 넘긴 상태이므로 바로 더할 수 있음
    ADD_TEN(state, payload) {
      state.total += payload;
    },
    //payload 객체가 들어왔다.
    ADD_RANDOM(state, payload) {
      state.total += payload.num;
    },
  },
  actions: {
    //mutations를 호출
    //backend api와 통신을 하는곳, context라고 하는 만능 객체가 인자로 들어옴
    // addOne(context) {
    //   // console.log(context);
    //   context.commit("ADD_ONE");
    // },
    addOne({ commit }) {
      commit("ADD_ONE");
    },
    //비동기통신
    asyncAddOne({ commit }) {
      //ez하게 비동기 해보는 방법
      // setTimeout(function () {
      //   commit("ADD_ONE");
      // }, 2000);
      setTimeout(() => {
        commit("ADD_ONE");
      }, 1000);
    },
  },
  modules: {
    //여러개로 쪼개놓고 관리를 하는 곳
  },
});
=================================================================
components/SubjectView.vue
<template>
  <div>
    <button @click="addOneCount">{{ title }} + 1 - {{ count }}</button>
    <button @click="addTenCount">{{ title }} + 10 - {{ count }}</button>
    <button @click="addRandomCount">{{ title }} + ? - {{ count }}</button>
    <button @click="asyncAddOne">{{ title }} 비동 - {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: "SubjectView",
  props: {
    title: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addOneCount() {
      this.count += 1;
      //action이라고 하는것을 호출을 할텐데...
      //mutations와 구분하기 위해서 메서드 이름을 카멜케이스를 사용했다.
      this.$store.dispatch("addOne");
    },
    addTenCount() {
      this.count += 10;
      //값을 인자로 넘겼다.
      this.$store.commit("ADD_TEN", 10);
    },
    addRandomCount() {
      let num = Math.round(Math.random() * 100);
      this.count += num;
      //객체를 인자로 넘겼다.
      this.$store.commit("ADD_RANDOM", { num });
    },
    //비동기도 actions를 호출해
    asyncAddOne() {
      this.count += 1;
      this.$store.dispatch("asyncAddOne");
    },
  },
};
</script>

<style></style>
=================================================================
components/ResultView.vue
<template>
  <div>
    <h2>전체 {{ total }} 번 클릭됨</h2>
    <h3>{{ countMsg }}</h3>
  </div>
</template>

<script>
//헬퍼를 이용하면 굉장히 편리해진다.
import { mapGetters } from "vuex";

export default {
  name: "ResultView",
  computed: {
    //물론 state도 가넝하다.
    //mapState라는걸 가져오면 (ㅎㅎ 직접 해볼것)
    total() {
      return this.$store.state.total;
    },
    ...mapGetters(["countMsg"]),
  },
};
</script>

<style></style>
=================================================================
App.vue
<template>
  <div id="app">
    <h2>당신이 좋아하는 파트를 선택하세요.</h2>
    <result-view></result-view>
    <subject-view title="코딩"></subject-view>
    <subject-view title="알고"></subject-view>
  </div>
</template>

<script>
import ResultView from "./components/ResultView.vue";
import SubjectView from "./components/SubjectView.vue";

export default {
  name: "App",
  components: {
    ResultView,
    SubjectView,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

세번째까지 위와 동일하고 네번째 버튼을 누르면 1초 뒤에 누른게 올라간다.

'vue.js' 카테고리의 다른 글

axios응용  (0) 2023.05.12
Vuex응용(TodoList만들기)  (0) 2023.05.12
Vue Style Guide  (0) 2023.05.10
Vue Axios  (0) 2023.05.10
Vue Router  (0) 2023.05.09