데브코스

[Day 37-2] Provide/Inject, Vuex(Store)

라다디 2022. 12. 10. 21:25

Provide / Inject

일반적으로 데이터를 부모에서 자식 컴포넌트로 전달할 때 props를 사용한다.

만일 조상 요소에서 후손 요소로 데이터를 전달할 때 props로 일일이 전달한다면 굉장히 복잡해질 것이다.

이럴 때 사용할 수 있는 것이 provideinject이다.

 

부모 컴포넌트는 데이터 제공을 위해 provide(제공)옵션을 사용하며, 자식 요소는 데이터 사용을 위해 inject(주입) 옵션을 사용한다.

<!-- App.vue -->
<script>
import Parent from '~/components/Parent';

export default {
  components:{
    Parent,
  },
  provide() {
    return {
      msg: this.msg, // provide로 데이터를 후손 요소로 보냄
    };
  },
  data() {
    return {
      msg: "App Vue!",
    };
  },
};
</script>
<!-- Child.vue -->
<template>
  <h1>Child {{ msg }}</h1>
</template>

<script>
export default {
  inject: ["msg"], // inject로 조상 요소에서 보내준 데이터를 받음
};
</script>

하지만 provide inject를 사용해서 전달된 데이터는 반응형 데이터가 아니다.

 

반응형 작업

상위 컴포넌트의 변경사항에 반응하게 하려면 computed 함수를 사용한다.

<!-- App.vue -->
<script>
import Parent from '~/components/Parent';
import { computed } from "vue";

export default {
  components:{
    Parent,
  },
  provide() {
    return {
      msg: computed(() => this.msg),
    };
  },
  data() {
    return {
      msg: "App Vue!",
    };
  },
};
</script>
<!-- Child.vue -->
<template>
  <h1>Child {{ msg.value }}</h1>
</template>

computed 함수를 사용하게 되면 계산된 데이터 객체가 반환이 된다.

따라서 계산된 데이터의 값을 꺼내기 위해서는 .value를 붙여야 한다.

 

provide와 inject를 사용하면 코드의 추적이 어려워지기 때문에 일반 애플리케이션에서 사용되는 것이 권장되지는 않는다.

 

Vuex(Store)

여러 컴포넌트에서 동시에 사용되는 데이터별도의 파일에 저장해서 전역으로 사용할 수 있다.

관리하고 사용할 데이터state, 상태라고 부른다. 

// store/index.js
import { reactive } from "vue"; // 반응형 데이터를 제공해주는 vue 내장 함수

export const state = reactive({
  msg: "Hello vue",
  count: 1,
});

export const getters = {
  // computed (계산된 데이터) 로직
  reversedMsg() {
    return state.msg.split("").reverse().join("");
  },
};

export const mutations = {
  // 데이터를 변경하는 로직
  increaseCount() {
    state.count += 1;
  },
  decreaseCount() {
    state.count -= 1;
  },
  updateMsg(newMsg) {
    state.msg = newMsg;
  },
};

export const actions = {
  // 기타 로직
  async fetchTodo() {
    const fetchResult = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    ).then((res) => res.json());

    mutations.updateMsg(fetchResult.title);
  },
};

이렇게 별도의 파일로 관리하게 되면 다양한 컴포넌트에서 데이터를 사용할 수 있고 관리가 용이하다.

 

컴포넌트에서는 아래와 같이 store의 데이터를 사용할 수 있다.

<template>
  <h1>Hello.vue</h1>
  <div>{{ reversedMsg }}</div>
  <div @click="increaseCount">
    {{ count }}
  </div>
  <button @click="fetchTodo">
    Get Todo
  </button>
</template>

<script>
import { state, getters, mutations, actions } from '~/store';
export default {
  data() {
    return state;
  },
  computed:{
    reversedMsg: getters.reversedMsg
  },
  methods:{
    increaseCount: mutations.increaseCount,
    fetchTodo: actions.fetchTodo
  },
};
</script>

이러한 데이터 관리 로직을 조금 더 쉽게 해줄 수 있게 도와주는 라이브러리가 바로 Vuex이다.

 

Vuex상태 관리 패턴 라이브러리이다.

Vue 어플리케이션의 모든 컴포넌트에 대한 중앙 집중식 저장소(store) 역할을 하게 된다. 

 

Vuex는 다음 명령어로 설치한다.

npm i vuex@next

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

const app = createApp(App)
app.use(store) // vuex는 일종의 플러그인
app.mount('#app')
// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state() {
    return {
      msg: 'Hello Vue?!',
      count: 1
    }
  },
  getters: {
    reversedMsg(state) {
      return state.msg.split('').reverse().join('')
    }
  },
  mutations: {
    increaseCount(state) {
      state.count += 1
    },
    updateMsg(state, newMsg) {
      state.msg = newMsg
    }
  },
  actions: {
    // context => state, getters, commit, dispatch
    async fetchTodo(context) {
      const todo = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json())
      console.log(todo)
      // commit(mutation 이름, 갱신할 데이터)
      context.commit('updateMsg', todo.title)
    }
  }
})

 

state (상태)

store에서 다루는 데이터이다.

데이터는 함수로 만들어서 반환해야 참조 관계의 문제가 발생하지 않는다.

 

getters (계산된 데이터)

상태에 대한 계산된 결과를 얻어내고 싶을 때 사용한다.

하나의 상태에 의존해서 데이터를 만들어낸다.

첫 번째 인수로 해당 state를 참조할 수 있는 매개변수가 제공된다.


mutations (상태 변경)

데이터, 즉 상태를 변이시킬 때 mutaions의 도움을 받아서 변환한다. (상태의 변화를 추적하는 곳)

첫 번째 인수로 해당 state를 참조할 수 있는 매개변수가 제공된다.


actions (기타 로직)

store에서 가지고 있는 데이터를 활용해서 처리할 수 있는 대부분의 로직이 들어가는 부분이다.

첫 번째 인수로 다른 옵션들에 접근이 가능context를 제공한다.
context에는 상태를 참조하는 state, 계산된 상태를 참조하는 getters, mutation을 실행할 수 있는 commit, 다른 action을 실행할 수 있는 dispatch가 있다.

 

mutaions동기적으로 동작하고, actions비동기적으로 동작한다.

 

모듈화

index.js라는 하나의 파일에서 프로젝트에서 사용하는 모든 데이터를 다루면 파일의 크기가 너무 커지고 관리하기가 어렵다.

이럴 때 사용할 수 있는 것이 모듈이라는 개념이다. 

 

store에서 모듈을 사용하기 위해서는 모듈의 옵션으로 namespaced의 값을 true로 줘야 한다.

// store/message.js
export default {
  namespaced: true, // 이름 범위로 데이터들을 사용할 수 있는 옵션
  state() {
    return {
      message: 'Hello Store Module~~',
    }
  },
  getters: {
    reversedMessage(state) {
      return state.message.split('').reverse().join('')
    }
  },
  mutations: {
    updateMessage(state, newMessage) {
      state.message = newMessage
    }
  },
  actions: {
    async fetchTodo({commit}) {
      const todo = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json())
      commit('updateMessage', todo.title)
    }
  }
}

 

그리고 index.js에서 modules 옵션을 추가하고 모듈의 이름을 작성한다.

import { createStore } from 'vuex'
import message from './message'
import count from './count'

export default createStore({
  state() {
    return {
      msg: 'Hello Vue?!',
    }
  },
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    message,
    count
  }
})

 

모듈화한 store의 state를 다루기 위해서는 아래와 같은 방식을 사용한다.

 

stategetters는 computed에 등록한다.

  • state this.$store.state.NameSpace.State
  • getters this.$store.getters['NameSpace/Getter']

 

mutationsactionsmethods에 등록한다.

  • mutations this.$store.commit('NameSpace/Mutation')
  • actions this.$store.dispatch('NameSpace/Action')
<template>
  <h1>Hello.vue</h1> 
  <div>{{ count }}</div>
  <button @click="increaseCount">
    +
  </button>
  <button @click="decreaseCount">
    -
  </button>
  <div>{{ message }} / {{ reversedMessage }}</div>
  <button @click="fetchTodo">
    Fetch Todo!
  </button>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count.count
    },
    message() {
      return this.$store.state.message.message
    },
    reversedMessage() {
      return this.$store.getters['message/reversedMessage']
    }
  },
  methods: {
    increaseCount() {
      this.$store.commit('count/increaseCount')
    },
    decreaseCount() {
      this.$store.commit('count/decreaseCount')
    },
    fetchTodo() {
      this.$store.dispatch('message/fetchTodo')
    }
  }
}
</script>

<style scoped lang="scss">
$color: red;
h1 {
  color: $color;
}
</style>

 

매핑

위 코드를 조금 더 간결하게 작성하기 위해 매핑 기능을 활용할 수 있다.

vuex의 내장 함수 mapState, mapGetters, mapMutations, mapActions를 활용하여 다음과 같이 작성한다.

<template>
  <h1>Hello.vue</h1> 
  <div>{{ count }}</div>
  <button @click="increaseCount">
    +
  </button>
  <button @click="decreaseCount">
    -
  </button>
  <div>{{ message }} / {{ reversedMessage }}</div>
  <button @click="fetchTodo">
    Fetch Todo!
  </button>
  <div>{{ msg }}</div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['msg']), // 전역 store의 경우 네임스페이스를 작성하지 않음
    ...mapState('count', ['count']),
    ...mapState('message', ['message']),
    ...mapGetters('message', ['reversedMessage'])
  },
  methods: {
    ...mapMutations('count', ['increaseCount', 'decreaseCount']),
    ...mapActions('message', ['fetchTodo'])
  }
}
</script>

 


출처: 프로그래머스 프론트엔드 데브코스 

[Day 37] Vue (5)