반응형
Prop Drilling
일반적으로 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달해야 할 때 props를 사용합니다. 그러나 큰 컴포넌트 트리가 있고 깊이 중첩된 컴포넌트에 먼 조상 컴포넌트의 무언가가 필요한 경우 props 만 있으면 전체 부모 체인에 동일한 prop을 전달해야 합니다.
graph TD;
Root --> Header;
Root --> Main;
Root --> Footer;
Footer --> DeepChild
Root -- Props ----> Footer;
Footer -- Props ----> DeepChild;
<Footer> 컴포넌트는 이 prop이 전혀 필요하지 않을 수 있지만, <DeepChild>가 접근할 수 있도록 prop을 선언하고 전달해야 합니다. 더 긴 상위 체인이 있으면 그 과정에서 더 많은 컴포넌트가 영향을 받게 됩니다. 이것을 Prop Drilling이라고 합니다.
이러한 Prop Drilling을 provide와 inject로 해결할 수 있습니다. 하위 트리의 모든 컴포넌트는 깊이에 상관없이 상위 체인의 컴포넌트에서 제공(provide)하는 의존성을 주입(inject) 할 수 있습니다.
graph TD;
Root --> Header;
Root --> Main;
Root --> Footer;
Footer --> DeepChild
Root -- provide ----> DeepChild;
DeepChild -- inject ----> Root;
Provide
컴포넌트의 하위 항목에 데이터를 제공합니다.
<script setup lang="ts">
import { provide } from 'vue';
provide(/* 키 */ 'message', /* 값 */ 'Hello, world!');
</script>Inject
부모 컴포넌트에서 제공하는 데이터를 주입하기 위해 사용합니다.
<script setup lang="ts">
import { inject } from 'vue';
const message = inject('message');
</script>provide와 inject를 이용하여 간단한 Form 컴포넌트를 작성하고, zod를 이용하여 스키마 선언 및 유효성을 검사해 보겠습니다.
Form 컴포넌트 작성
심볼 키 사용하기
잠재적 충돌을 피하기 위해 제공 키로 Symbol을 사용합니다.
// keys.ts
export const formErrorsInjectionKey = Symbol();컴포넌트 구조
graph TD;
VForm --> VFieldSet;
VFieldSet --> VField;
VForm -- provide ----> VField;
VField -- inject ----> VForm;
컴포넌트 작성
<!-- /components/VForm.vue -->
<script setup lang="ts">
import { z } from 'zod';
import { formErrorsInjectionKey } from '~/keys';
interface Props {
schema: any;
values: z.infer<any>;
}
const props = defineProps<Props>();
const emit = defineEmits(['submit', 'error']);
const errors = ref<{
[key in keyof z.infer<typeof props.schema>]?: string | undefined;
}>({});
const handleSubmit = async () => {
const result = await props.schema.safeParseAsync(props.values);
if (result.success) {
errors.value = {};
emit('submit', result.data);
} else {
errors.value = Object.fromEntries(
Object.entries(result.error.flatten().fieldErrors).map(([k, v]: any) => [
k,
v?.[0]
])
);
emit('error', errors.value);
}
};
provide(formErrorsInjectionKey, readonly(errors));
</script>
<template>
<form @submit.prevent="handleSubmit">
<slot />
</form>
</template>
<style scoped lang="scss"></style>
<!-- /components/VFieldSet.vue -->
<script setup lang="ts">
interface Props {
disabled?: boolean;
}
withDefaults(defineProps<Props>(), {
disabled: false
});
</script>
<template>
<fieldset :disabled>
<slot />
</fieldset>
</template>
<style scoped lang="scss"></style>
<!-- /components/VField.vue -->
<script setup lang="ts">
import { formErrorsInjectionKey } from '~/keys';
interface Props {
label?: string;
name: string;
tag?: string;
}
const props = withDefaults(defineProps<Props>(), {
tag: 'div'
});
const errors = inject<{ readonly [k in Props['name']]?: string | undefined }>(
formErrorsInjectionKey,
{} // default value
);
</script>
<template>
<component :is="tag">
<label v-if="label">{{ label }}</label>
<div>
<slot />
<slot name="error" :props="errors[props.name]">
<div v-if="errors[props.name]">{{ errors[props.name] }}</div>
</slot>
<slot name="hint">
<div v-if="hint && !errors[props.name]">{{ hint }}</div>
</slot>
</div>
</component>
</template>
<style scoped lang="scss"></style>스키마 선언 및 유효성 검사
<script setup lang="ts">
import { z } from 'zod';
interface Schema {
id?: string;
password?: string;
}
const schema = z
.custom<Schema>()
.refine(({ id }) => id, {
message: '아이디를 입력해주세요.',
path: ['id']
})
.refine(({ password }) => password, {
message: '비밀번호를 입력해주세요.',
path: ['password']
});
const values = reactive<z.infer<typeof schema>>({
id: '',
password: ''
});
const { id, password } = toRefs(values);
const handleSubmit = (values: z.infer<typeof schema>) => {
try {
// ...
} catch (error) {
// ...
}
};
</script>
<template>
<VForm :schema :values @submit="handleSubmit">
<VFieldset>
<div>
<VField label="ID" name="id">
<input v-model="id" />
</VField>
<VField label="Password" name="password">
<input v-model="password" type="password" />
</VField>
</div>
<div>
<button type="submit">Submit</button>
</div>
</VFieldset>
</VForm>
</template>
<style scoped lang="scss"></style>반응형