[VUE3 신기능 시리즈#2] Prop의 진화된 버전 : Provide(제공) / Inject(주입)

안녕하세요, 서비스개발팀 Yohan입니다. 🫡  우리가 Vue.js를 활용하여 웹페이지를 만들 때 각 컴포넌트로 값을 전달하려면 props를 주로 사용해왔는데요, props는 사용하기 간편한 장점이 있지만 때로는 불편할 때가 있습니다. 이를 해결하기 위해 Vue.js 3버전에서는 ProvideInject의 개념이 등장했는데요. 이번 주제는 ProvideInject가 무엇인지, 어떤 상황에 사용하면 유용한지에 대해 알아보는 시간을 가져보겠습니다.

Prop 드릴링

예를 들어 상위 컴포넌트 Root 아래 하위 컴포넌트 Footer가 있고, 또 그 하위 컴포넌트 Footer를 상위 컴포넌트로 가지는 하위 컴포넌트 DeepChild가 있다고 했을 때, Root에서 DeepChild로 값을 전달하려면 기존 사용하던 Props 체계에서는 전체 부모체인에 동일한 prop을 전달해야 했습니다.

Root → Footer → DeepChild 순으로 동일한 prop를 전달해야 했던 지난날들..

위와 같은 경우, Footer 컴포넌트에서는 prop 이 전혀 필요하지 않을 수 있는 상황이지만, 하위 컴포넌트인 DeepChild 에게 값을 전달하기 위해 어쩔 수 없이 부모 컴포넌트로부터 prop 을 전달받아 다시 아래로 전달해야 합니다. 이러한 비효율적인 구조를 개선하기 위해 나온 개념이 provideinject 입니다. ✨

Root에서 provide한 값을 중간단계를 거치지 않고 바로 하위 컴포넌트에 inject하는 새로운 방식

Provide

컴포넌트의 하위 항목에 데이터를 제공하려면 provide() 함수를 사용하면 됩니다.

<script setup>
import { provide } from 'vue'

provide(/* 키 */ 'message', /* 값 */ '안녕!')
</script>

provide() 함수는 두 개의 파라미터를 허용합니다. 첫 번째 파라미터는 주입 키로 문자열 이나 Symbol 이 될 수 있습니다. 이 주입 키는 자식 컴포넌트에서 필요한 값을 조회할 때 사용됩니다. 두 번째 파라미터는 제공되는 값입니다. 값은 refs와 같은 반응형 상태를 포함하여 모든 값이 될 수 있습니다.

ex)

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

반응형 값을 provide 로 제공하면, 자식 컴포넌트가 반응형 연결을 설정할 수 있습니다.

Inject

상위 컴포넌트에서  제공한 데이터를 하위 컴포넌트에서 받을 때는 inject() 함수를 사용합니다.

<script setup>
import { inject } from 'vue'

const injectedCount = inject('count')

console.log(injectedCount);  // 0 provide에 설정한 값
</script>

⚡주입 시 기본 값 설정하기

기본적으로 inject 는 주입된 키 값이 상위 컴포넌트 어딘가에서 제공된다고 가정합니다. 만약 상위 컴포넌트에서 provide 된 데이터가 존재하지 않으면 런타임 오류가 발생합니다. 이러한 오류를 피하기 위해서는 props 처럼 기본값을 선언해야 합니다.

// `value` 값은 0이 됩니다.
// "message"에 해당하는 데이터가 제공되지 않은 경우
const value = inject('count', 0)

반응형으로 만들기

provide를 반응형 값으로 사용할 때, 값을 주입하는 쪽이나 받는 쪽 모두 변경사항을 반응형 상태로 유지하는 것이 좋습니다. 이렇게 하면 props와는 달리 제공받은 값을 하위 컴포넌트에서도 변경할 수 있으며, 이러한 변경된 값은 상위 컴포넌트에서도 동일하게 반영됩니다.

하위 컴포넌트에서 inject 받은 값을 변경하려면 상위 컴포넌트에서 provide 할 때 상태 변경을 담당하는 함수를 제공하면 됩니다.

ex)

// Main.vue 
<script setup>
import { provide, ref } from 'vue';
import Children from './Children.vue';

const legacyFromMe = ref('할아버지의 유산 $100만');
const updateLegacy = () => {
	legacyFromMe.value = '할아버지의 유산 $1만';
};

provide('legacy', { legacyFromMe, updateLegacy });
</script>

<template>
	<div class="p-3">
		<h1>할아버지 페이지</h1>

		<Children />
	</div>
</template>
// Children.vue
<template>
	<div>
		<h1>Children</h1>
		<p>자식 컴포넌트</p>
		<GrandChildren />
	</div>
</template>

<script setup>
import GrandChildren from './GrandChildren.vue';
</script>
// GrandChildren.vue
<template>
	<div>
		<h1>GrandChildren</h1>
		<p>손자 컴포넌트</p>
		<p class="injected-from-grand">
			할아버지가 Provide 해서 inject로 주입한 값 : {{ legacyFromMe }}
		</p>

		<button @click="updateLegacy">값을 변경해 봅시다</button>
	</div>
</template>

<script setup>
import { inject } from 'vue';

const { legacyFromMe, updateLegacy } = inject('legacy');
</script>

위 예제에서는 Main.vue 파일에서 provide 함수를 사용하여 legacyFromMe 변수와 updateLegacy 함수를 제공합니다. 그런 다음, GrandChild.vue 파일에서 inject 함수를 사용하여 해당 값을 받아옵니다.

그리고 값을 변경하는 버튼을 클릭할 때 상위 컴포넌트에서 제공한 함수를 호출하여 legacyFromMe 변수의 값을 변경시킵니다. 이를 통해 상위 컴포넌트와 하위 컴포넌트 간에 데이터를 동기화할 수 있습니다.

여기서 주의할 점은, 상위 컴포넌트에서 provide할 때 넣은 변수의 이름을 하위 컴포넌트에서 가져올 때 사용하는 변수 이름을 동일하게 유지해야 한다는 것입니다.

하지만 props 처럼 상위 컴포넌트를 통해 전달된 데이터를 하위 컴포넌트에서 변경할 수 없도록 하려면, 제공된 값을 readonly() 함수로 래핑할 수 있습니다.

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

심볼 키 사용하기

지금까지 예제에서는 문자열 키를 사용했습니다. 하지만 대규모 프로젝트를 작업하거나, 다른 개발자가 사용할 컴포넌트를 작성하는 경우에는 잠재적 충돌을 피하기 위해 문자열 키 대신 Symbol 을 사용하는 것이 권장됩니다.

Symbol은 유일한 값을 가지므로 충돌없이 고유한 식별자로 사용할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시키며, 의도치 않은 데이터 덮어쓰기를 방지할 수 있습니다.

// keys.js
export const myInjectionKey = Symbol()
// 제공하는 곳의 컴포넌트에서
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, {
  /* 제공할 데이터 */
})
// 주입되는 곳의 컴포넌트에서
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

위의 예제와 같이 키로 사용할 문자열 대신 Symbol 을 관리하는 자바스크립트 파일을 따로  만들고 해당 파일에서 선언된 Symbol을 키로 사용함으로써 충돌을 방지할 수 있습니다.

결론

지금까지 Vue.js 3버전에서 새롭게 태어난 기능인 ProvideInject 기능에 대해 살펴봤습니다. 저도 지금까지 개발을 하다보면 하위 컴포넌트가 중첩된 페이지가 생겼을 때, 해당 하위 컴포넌트에 값을 내려주려고 해당 값을 사용하지 않는 컴포넌트에까지 props 를 내려가며 작업했던 기억이 떠오르는데요. ProvideInject 기능을 사용하면 더이상 그런 불필요한 과정 없이 상위 컴포넌트에서 바로 필요한 하위 컴포넌트에 값을 전달할 수 있게 되어 아주 편리하게 개발을 할 수 있게 되었습니다.

앞으로도 더욱 다양한 Vue.js의 새로운 기능들에 대해 소개하며 Vue.js를 사용하는 프론트엔드 개발자분들에게 조금이나마 도움이 될 수 있기를 바랍니다.  그럼 오늘의 포스팅을 마무리하겠습니다! 🫡

참고

Vue.js

헥토데이터는 데이터 기반 다양한 서비스를 지원하는 기업입니다. 온라인에 분산된 데이터를 실시간으로 제공하기 위해 클라이언트 엔진과 웹 API를 활용합니다. 유수의 비대면 대출 핀테크 서비스가 선택한 헥토데이터의 CODEF API. 꼼꼼한 보안과 빠른 대응으로 기업이 자사 서비스에만 집중할 수 있도록 돕는 CODEF API에 대해 아래 배너를 눌러 확인해 보세요.

본 페이지 내의 모든 콘텐츠는 저작권법에 의해 보호받는 저작물로서, 모든 사용 권리는 ㈜헥토데이터에게 있습니다. 별도의 저작권 표시 없이 무단으로 사용하는 것을 금지하며, 자세한 저작권 정책은 해당 링크를 참고하시기 바랍니다. Copyright 2024.㈜헥토데이터 All rights reserved.