BeomSeo

Published

- 9 min read

vue3에서의 Provider Pattern 올바르게 사용하기

img of vue3에서의 Provider Pattern 올바르게 사용하기

이 패턴을 사용하는 이유?

생각보다 프로젝트를 진행하면서 vue의 provide inject를 사용할때 팀원들이 혼란스러워하는것이 많다고 느껴졌기에 간단하게 정리하고자 한다.

Provider 패턴은 컴포넌트 트리 상위에서 데이터를 제공하고, 하위 컴포넌트들이 이를 소비하는 방식으로 동작하는 디자인 패턴이다. 이 패턴을 사용하면 props drilling(여러 단계의 컴포넌트를 거쳐 데이터를 전달하는 과정)을 피하고, 필요한 곳에서 데이터를 직접 접근할 수 있어 코드의 가독성과 유지보수성이 향상된다.

리액트에서의 사례

react에서는 컴포넌트로만 Provider를 사용할 수 있지만 Vue에서는 composition에서 사용할 수 있기에 유연한 부분이 있다.

   // UserProvider.tsx
import { useState } from 'react'
const UserProvider = ({ children }) => {
	const [name, setName] = useState('World')
	const value = {
		state: { name },
		actions: { setName }
	}
	return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

// useUser.ts
import { useContext } from 'react'
import UserContext from './context'

const useUser = () => {
	return useContext(UserContext)
}

react에서는 Context.Provider컴포넌트를 사용하여 Provider를 만들고 해당 값을 사용하려면 useContext hook을 이용해 값을 가져올 수 있다.

다만 value값 변경시에는 하위 컴포넌트들은 직접 의존하지 않더라도 반드시 리렌더링이 된다는 단점이 존재한다.(리액트 한정)
이런 문제를 해결하려면 별도로 redux처럼 root store에 대한 참조는 한번 생성하고 주입한후 store내의 변경을 자체적인 상태관리 방식으로 처리하고,
의존하는 코드도 마찬가지로 해당 store를 subscribe하는 코드를 만듬으로써 반응형 코드로 구현해야 한다

Vue에서 구현한다면?

   import { ref, provide } from 'vue'

function providerUser() {
	// 공유할 상태 정의
	const user = ref({
		name: '홍길동',
		email: 'hong@example.com'
	})
	// provide를 사용하여 상태 제공
	provide('user', user)
}

import { inject } from 'vue'
const useUser = () => {
	return inject('user')
}

vue는 react와 다르게 컴포넌트 방식으로 provide하지 않고 composition 방식으로 provide할 수 있기 때문에 좀더 간편한 부분이 있다

올바르게 사용하기

오늘 이 글을 작성하게 된 이유는 팀원들중에 단일 event handler를 전달하는데 있어서도
provide를 사용하는 케이스를 봤기 때문이다.

안좋게 구현한 케이스

   provide('onSelectItem', () => {
	/** ... */
})

일단 구현 사례는 공통 테이블 그리드를 만들고
해당 테이블그리드 컴포넌트를 fsd패턴으로 래핑하였고
이벤트핸들러를 binding하는데 있어서 entities에서 구현하고 shared에 있는 TableGrid를 한번더 래핑하는 구조이기에 props drilling을 피하기 위해 그렇게 사용하였다고 했다.

이 방식이 좋지 않다고 생각하는 부분들에 대해서 적자면

interface가 명시적으로 노출되지 않는다.

어떤값을 inject 받아서 사용하는지 컴포넌트 인터페이스가 아니라 코드나 경고,문서를 보고 알아차려야 하는점이 좋지 못하다.

   provide(
	'onSelectItem' /* 무슨 key를 입력해야하는지 코드레벨에서는 바로 알기 힘듬 */,
	(param /* 무슨 타입인지는 알아서 찾아서 typing 하기*/) => {
		/** ... */
	}
)

어떤 모듈에 대한 Provider가 아니라 단일값을 provide inject 하기 위한 목적이면 오히려 복잡하다.

위 코드 처럼 사용하는 사람의 입장이라면 어떤 키를 기반으로 값을 입력해줘야 하는지 번거롭게 찾기보다는
명시적인 코드로 interface가 제공되는것이 더 나을것이다.

   // 명시적으로 parameter가 노출되는 인터페이스
provideTable({
	// 테이블에서 무슨 event를 받는지 알 수 있다.
	onSelectItem(param /* 암시적으로 자동 제공되는 타입정보 */) {}
})

컴포넌트에 대한 로직을 굳이 provide로 제공할 이유가 없을때

해당 컴포넌트와 강하게 엮여 있는 코드라면
대부분 vue에서 fallthrough로 event와 props를 전달하는 방식이 훨씬 나을것이다.

올바르게 구현한 케이스

1. 단일 속성을 전달하기 보다는 목적에 맞는 속성과 로직이 모여있는 composable들을 내려주는것이 낫다.

provide와 inject는 명시적으로 설계된 상태와 로직을 컴포넌트 간 공유할 때 유용하다
단일 값만 전달하는 방식보다는 관련된 상태와 로직이 결합된 composable을 제공하는 것이 사용성과 유지보수 측면에서 더 낫다.

   import { ref, provide, inject, onUnmounted } from 'vue'

// ssr환경에서는 hydration시 symbol이슈가 날 수 있기에 string으로 처리한다.
const PopoverSymbol = Symbol('Popover')

export function providePopover() {
	const isVisible = ref(false)
	const anchorEl = ref(null)

	const show = (anchor) => {
		anchorEl.value = anchor
		isVisible.value = true
	}

	const hide = () => {
		isVisible.value = false
		anchorEl.value = null
	}

	const toggle = (anchor) => {
		if (isVisible.value) {
			hide()
		} else {
			show(anchor)
		}
	}

	// Provide the state and actions
	provide(PopoverSymbol, {
		isVisible,
		anchorEl,
		show,
		hide,
		toggle
	})
}

export function usePopover() {
	const popover = inject(PopoverSymbol)
	if (!popover) {
		throw new Error('usePopover must be used within a providePopover')
	}
	return popover
}

2. 컴포넌트가 강하게 연관된 경우, props와 events를 통해 명시적으로 처리한다.

테이블과 이벤트가 강하게 연관되어 있고,
이 둘이 별도로 분리될 필요가 없는 경우 props와 emit을 활용하는 것이 더 직관적이다.

   <template>
	<table>
		<tr v-for="item in items" :key="item.id" @click="handleSelectItem(item)">
			<td>{{ item.name }}</td>
		</tr>
	</table>
</template>

<script>
export default {
	props: {
		items: {
			type: Array,
			required: true
		}
	},
	emits: ['select-item'],
	methods: {
		handleSelectItem(item) {
			this.$emit('select-item', item)
		}
	}
}
</script>

위 컴포넌트를 사용하게 될때 사용자는 어떤 이벤트가 필요한지 어떤 props가 필요한지 코드를 직접 보지 않고도 알 수 있다.

결론

사실 다른 사용사례 코드를 본다면 당연한거 아닐까 싶은 내용이라고 생각했지만

이것으로도 생각이 다르기에 내용을 정리해봤다.

좋은 component를 작성하는것이나, 좋은 composition hook등을 작성하는것은 한번에 하기는 쉽지는 않다.

provide/inject는 상황에 따라 적절히 사용하되, 단순한 값 전달만을 위해 사용하지 않도록 주의하자.