IT/언어

[Vue3] Provide/Inject 사용법

개발자 두더지 2023. 9. 14. 23:39
728x90

일본의 한 블로그 글을 번역한 포스터입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 이번 포스트의 대상 독자는 다음과 같다.

  • Vue3의 상태 관리에 대해서 알고 싶은 분
  • Vue3의 작성법에 대해서 알고 싶은 분
  • 실무에서 사용할 것같으니, 당장 습득할 필요가 있는 분

 

 

만들고자 하는 어플리케이션 이미지


 이번에는 버튼을 누르면 숫자가 올라가거나 내려가는 어플리케이션을 만들고자한다. Vuex를 이용한 경우와 비교하면 꽤 알기 쉬울 것이다. Vuex를 이용하는 경우 다음과 같이 구성하게 될 것이다.

store/index.ts

 state오브젝트안에 초기값을 설정한다.

count: 0

mutation 오브젝트안에는 이와 같이 count 업과 다운 메소드를 준비한다.

increment(state) => void
decrement(state) => void

 그 후에는 getters오브젝트를 호출원으로 사용하는 getter 메소드를 준비하면 각 컴포넌트에서 호출이 가능하고, 글로벌한 상태 관리를 실현할 수 있다. 

 그러나 이번 포스트에서는 Vuex를 사용하지 않고, Vue3에서 기본적으로 제공하고 있는 Provide/Inject를 사용한 방법에 대해서 자세히 설명하고자 한다. 그러면 본격적으로 살펴보자!

 

 

Provide / Inject의 상용법


먼저 Vuex에서 말하는 Store를 만들자. ts 파일로 작성한다.

 src 디렉토리 바로 아래에 "providers" 디렉토리를 만들자. 그리고 그안에 ts 파일을 만든다. 이름은 어떤 것이든 상관없다. 여담으로 React의 이야기이지만, React에서 providers 디렉토리 안에는 "useXXXProvider.ts"이라는 이름을 붙이는 것이 일반적인 룰이므로, 이를 따라도 좋다.

 

state, action의 설정

type State = {
  count: number;
};

 먼저 state의 형을 정의한다. 콤마가 아닌, 세미클론으로 구분하므로 주의하자.

 Vuex에서는 잘 하지 않는 방법이므로, 처음에는 무엇을 하는지 감이 잘 안올지도 모르겠지만, 형을 정의하는 것으로 실제로 state의 속성을 입력할 때에 형의 보완해주는 역할을 한다.

 state안에 추가할 때는 동시에 데이터형을 추가할 필요가 있는 것을 잊어버리면 안된다. 예를 들어, State에 count 속성 그외에, message 속성을 추가하고 싶은 경우, 이렇게 작성해야한다는 것이다.

type State = {
	count: number;
	message: string;
}

 그럼 계속해서 store의 안의 코드도 작성해나가자. 전체 코드는 다음과 같다.

export const useStore = () => {
  //state
  const globalState = reactive<State>({
    count: 0,
  });

  //action
  const increment = () => {
    globalState.count++;
    console.log("Store안의 increment", globalState.count);
  };
  const decrement = () => {
    globalState.count--;
    console.log("Store안의 decrement", globalState.count);
  }
  
  return { ...toRefs(globalState), increment, decrement };
};

 각 코드에 대해서 해설하도록 하겠다. 먼저 state부분이다. 아마 익숙하지 않은 부분은 reactive<State>일 것이다. reactive는 Vue3에서 새롭게 추가된 기능이다.

 이게 없다면 호출원 컴포넌트 template태그안에 변경이 반영되지 않는다. 그리고, <State>는 오브젝트의 데이터형을 나타낸다. 이것을 기재함으로써 속성관련해서 데이터형 보완 효과를 줄 수 있다.

 계속해서 action에 대해서 설명하도록 하겠다. Vuex와 달리, action도 mutation도 구분이 없어졌다. 즉, state의 변경에 관해서 비동기의 관여에 대해서 신경쓸 필요가 없다는 것이다. 또한, getters도 없어졌다. 컴포넌트쪽에서 직접 action이나 mutation을 호출하는 방식으로 이용한다.

 메소드의 작성법은 화살표 함수가 추천되고 있다. 즉 일반적인 JavaScript 메소드 작성법으로 작성하면 된다. 

 마지막으로 export할 store의 메소드 마지막에 반드시 return이 필수이다. 무엇을 return하고 있는가에 대해 얘기하자면, state와 action 모두를 오브젝트 내에 저장하고, 그 오브젝트를 return하고 있다.

 이 코드와 관련해서 처음 볼 수도 있는 부분이 ...toRefs()일 것이다. 이것은 극단적으로 얘기하자면 ()안에 오브젝트의 속성을 ref를 붙여 따로 따로한다고 생각하면 된다. 모던 JavaScript를 배운 사람에게 설명하자면, 분할 대입한 속성이 ref오브젝트로 되어 있다고 생각하면 된다.

 컴포넌트 쪽에서 호출할 경우,  globalState.count가 아니라 count로 생략할 수 있다. 더욱이 count의 값을 확인하자면, 다음과 같이 ref 오브젝트로 되어 있다. 이로 인해 template 태그안에서도 변경이 반영되게 된다.

console.log(count)  //ref: { value:  0 }

 

key의 설정

 아직 끝나지 않았다. 새롭게 ts 파일을 설정할 필요가 있다. 방금봤던 state나 action가 기재된 store 메소드 외에 외부에서 액세스하기 위한 설정을 기재한다.

// 부모, 자식 컴포넌트에서 이용할key. return {  }의 데이터형 표현이 이부분.
type storeType = ReturnType<typeof useStore>;
export const storeKey: InjectionKey<storeType> = Symbol("store");

 첫 번째 행은 보면 알 수 있듯, 데이터형을 정의하고 있다. ReturnType이라는 데이터형을 vue쪽에서 제공하고 있기 때문에, 이것을 import아혀 return{}할 오브젝트 데이터형으로 정의한다. typeof로 데이터 형의 추측하여 정의한다.

 그리고 마지막은 key의 설정이다. 이것은 컴포넌트측에서 이 Store에 액세스할 때에 필요한 것이다. 따라도 export도 필요하다.

 InjectionKey이라는 데이터형을 vue에서 import한다. 그리고 그 데이형을 <>안에, 방금 첫 번째 정의한 store 메소드의 데이터형을 쓴다. 이로인해, store 키는 store메소드를 이용할 때의 Inject의 키로 인식된다.

 Symbol은 JavaScript에서 제공하고 있는 메소드이다. Key를 암호화하는 것으로 console.log로 출력해도 아무것도 나오지 않는다.

 한편, ts 전체 코드를 모아보면 다음과 같다.

import { InjectionKey, reactive, toRefs } from "vue";


type State = {
  count: number;
};

export const useStore = () => {
  //state
  const globalState = reactive<State>({
    count: 0,
  });

  //action
  const increment = () => {
    globalState.count++;
    console.log("Store안에서 increment", globalState.count);
  };
  const decrement = () => {
    globalState.count--;
    console.log("Store안에서 decrement", globalState.count);
  };

  return { ...toRefs(globalState), increment, decrement };
};

type storeType = ReturnType<typeof useStore>;
export const storeKey: InjectionKey<storeType> = Symbol("store");

 

provide의 설정

 드디어 타이틀 중에 하나만 provide가 등장했다. 여기서 하나의 vue 파일을 작성한다. 파일을 만드는 위치든 어디든 상관없다.

<template>
  <div>
    <slot />
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from "vue";
import { useStore, storeKey } from "@/provider/StoreProvider";
export default defineComponent({
  //setup의 안에 필드 변수도 메소드도 기재
  setup() {
    // 기본적으로는 루트 컴포넌트에 provide를 쓴다.
    // 부모 컴포넌트에서 필요. 첫 번째 인수는 はkey, 두 번째 인수는 Store.
    provide(storeKey, useStore());
    return {};
  },
});
</script>

 script 태그내를 보길 바란다. 사실 provider의 설정은 심플하다. provide를 vue에서 import하고, 동시에 방금 설정한 Key를 ts 파일에서 import한다. 그리고 setup함수내에서 provide 메소드를 호출하면 된다. 이 한 줄로 script태그 내의 설정은 끝이다.

 다음은 template태그안을 살펴보자. 여기에 <slot />을 쓰고 있다. 이것은 vue에서 제공하는 하나의 기능이고 vue2에서도 동일한 기능을 제공하고 있다. React를 배운 적이 있는 사람에게는 childern을 대신하는 것이라고 설명하면 알아듣기 쉬울지도 모른다. 이번 포스트에서는 slot이 메인이 아니므로 극단적으로 간단하게 slot에 대해 설명하자면 <slot /> 부분에 무언가를 치환하게 적용하는 것이다. 

 

Inject의 설정

 마지막으로 두 번째 키워드인 Inject이다. Inject의 설정도 간단하다. 이 한 줄이 끝이다.

Inject( 설정한Key )
<template>
  <div>
    <h4>카운트 영역</h4>
    <button v-on:click="onDecrement">-</button>
    <span>count의 값: {{ store.count.value }} </span>
    <button v-on:click="onIncrement">+</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";
import { storeKey } from "@/provider/StoreProvider";
export default defineComponent({
  //setup의 안에 필드 변수도 메소드도 기재
  setup() {
    //inject에는 export한 키가 들어간다.useStore의 안에 있는 state,action를 쓸 수 있다.
    const store = inject(storeKey);

    //store의 에러를 회피. undefinde의 가능성을 없애기 위해.
    if(!store){
      throw new Error("")
    }

    const onIncrement = () => {
      store.increment();
    };
    const onDecrement = () => {
      store.decrement();
    };

    return { store, onIncrement, onDecrement };
  },
});
</script>

참고자료

https://ryotablog.jp/provide-inject/

728x90