관리 메뉴

즐겁게, 코드

[Vue] inject-provide 패턴 조금 더 잘 써보기 본문

🎨 프론트엔드/Vue.js

[Vue] inject-provide 패턴 조금 더 잘 써보기

Chamming2 2024. 11. 19. 14:16

React에서는 Context.ProvideruseContext 로 구성된 Context API를 사용해 하위 컴포넌트로 값을 공유할 수 있듯, Vue에서도 provide ↔ inject 라는 함수(편의상 '패턴' 이라 부르겠습니다)를 조합해 값을 공유할 수 있습니다.

부모 컴포넌트에서 자식 컴포넌트로 값을 전파할 수 있지만, Provide 함수는 "카:값" 형태로 값을 관리하는 점이 다릅니다.

이번 글에서는 Provide - inject 패턴의 사용법을 간단히 알아보면서, 두 가지 팁을 함께 소개해보려 합니다. 

TL;DR

  • provide - inject 함수의 키로 심볼을 활용하면 키의 중복을 차단할 수 있다.
  • InjectionKey 타입을 활용하면 키에 따라 inject될 값의 타입을 미리 추론할 수 있다.

Provide - Inject 패턴의 예시

앱에 다크 모드 / 라이트 모드 / 시스템 테마에 알맞는 UI 색상을 적용하기 위해 테마 값을 전역으로 관리하는 모습입니다.

// Provider.vue

<script setup lang="ts">
import { provide, ref } from "vue";
import Consumer from "./Consumer.vue";

// 다크 테마, 라이트 테마, 시스템 설정 테마
const enum ThemePreference {
  DARK,
  LIGHT,
  SYSTEM,
}

export type Theme = { theme: ThemePreference };

const themeRef = ref<Theme>({ theme: "system" });

provide("theme", themeRef);
</script>

<template>
  <Consumer />
</template>
// Consumer.vue

<script setup lang="ts">
import { inject } from "vue";
import { Theme } from "./Provider.vue";

// inject 함수에 제네릭 타입을 부여해, 주입할 값의 타입을 추론할 수 있다.
const theme = inject<Theme>("theme");
</script>

<template>
  <div>현재 선택된 테마 : {{ theme?.theme }}</div>
</template>

<style scoped></style>

이 코드는 완벽하게 동작하지만, 조금 더 나은 DX를 위해 두 부분을 개선할 수 있습니다.

1. Symbol(심볼) 키 사용하기

하나는 공식 문서에서도 제안하는 심볼(Symbol) 키를 사용하는 방법입니다.

각각의 심볼은 고유함이 보장되므로 중복될 수 있는 문자열과는 달리 언제나 고유함이 보장됩니다.

💡 장점이 잘 와닿지 않는다면 CSS의 클래스명을 사용할 때와 Module CSS, Scoped CSS를 사용했을 때의 차이를 떠올려 보세요.
// constants/keys.ts

export const INJECT_THEME_KEY = Symbol("theme");
// Provider.vue

<script setup lang="ts">
import { provide, ref } from "vue";
import Consumer from "./Consumer.vue";
import { INJECT_THEME_KEY } from "./constants/keys";

const enum ThemePreference {
  DARK,
  LIGHT,
  SYSTEM,
}

export type Theme = { theme: ThemePreference };

const themeRef = ref<Theme>({ theme: "system" });

provide(INJECT_THEME_KEY, themeRef);
</script>

<template>
  <Consumer />
</template>
// Consumer.vue

<script setup lang="ts">
import { inject } from "vue";
import { Theme } from "./Provider.vue";
import { INJECT_THEME_KEY } from "./constants/keys";

// Symbol 키는 항상 고유함이 보장되어, 키가 중복되거나 네이밍을 고려해야 하는 수고를 덜 수 있다.
const theme = inject<Theme>(INJECT_THEME_KEY);
</script>

<template>
  <div>현재 선택된 테마 : {{ theme?.theme }}</div>
</template>

<style scoped></style>

2. InjectionKey 타입으로 값 타이핑하기

다음은 주입된 값의 타입을 선언하는 방법입니다.

// Consumer.vue

<script setup lang="ts">
import { inject } from "vue";
import { Theme } from "./Provider.vue";
import { INJECT_THEME_KEY } from "./constants/keys";

// 값을 inject해 사용할 때마다 <Theme> 제네릭을 명시적으로 선언해야 할까?
const theme = inject<Theme>(INJECT_THEME_KEY);
</script>

<template>
  <div>현재 선택된 테마 : {{ theme?.theme }}</div>
</template>

<style scoped></style>

제네릭으로 타입을 선언하지 않으면 속성을 추론할 수 없습니다.
타입 정의 후 속성이 추론되는 모습

제네릭으로 타입을 선언하면 강력한 타입 추론의 이점을 누릴 수 있지만, 값을 주입할 때마다 심볼 키에 대응하는 타입을 명시적으로 선언하는 작업은 유지보수에 불리합니다.

 

이를 개선하기 위해 InjectionKey 타입을 활용할 수 있습니다.

// constants/keys.ts
import { InjectionKey, Ref } from "vue";

// "inject(INJECT_THEME_KEY) 로 주입한 값은 Ref<Theme> 타입이야" 라고 선언했습니다.
export const INJECT_THEME_KEY: InjectionKey<Ref<Theme>> = Symbol("theme")

타입을 명시하지 않았지만 theme 속성이 추론됩니다.

마치며

강력한 Devtool 지원을 받고 싶거나 비동기 로직 또는 setup API 바깥에서 상태를 관리하는 등 복잡한 경우를 가정한다면 상태 관리 라이브러리인 pinia가 provide - inject 패턴보다 효과적인 선택이 될 수 있습니다.

 

하지만 *값이 생산되는 지점이 명확하면서 변하지 않음이 보장되면서 provide - inject 패턴을 사용하는 것이 보다 쉽고 강력한 방법이 될 수 있을 것이라 생각합니다. (Ex. 전역으로 사용하는 UI 컴포넌트의 참조를 등록하는 등)

type Key = "user" | "profile" | "cart";

provide("user") // provide의 키를 Key 값 중 하나로 좁힐 수는 없을까? (현재는 불가능)

inject("user") // inject의 키를 Key 값 중 하나로 좁힐 수는 없을까?

미래에는 이런 타입도 지원할 수 있길 바라며, 감기 조심하시길 바라겠습니다! 🥶

 

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆