Vue 3의 <script setup> 구문에서는 컴포넌트 인스턴스가 기본적으로 닫혀 있어 부모가 자식의 내부 상태에 직접 접근할 수 없습니다. 하지만 아키텍처 요구사항에 따라 부모 컴포넌트가 자식 컴포넌트의 데이터를 제어해야 하는 상황이 발생할 수 있습니다. 이를 구현하는 두 가지 주요 기술적 패턴을 살펴봅니다.
패턴 1: defineExpose와 ref를 활용한 직접 접근
자식 컴포넌트에서 defineExpose를 사용해 특정 상태를 외부로 노출하고, 부모 컴포넌트에서 템플릿 ref를 통해 인스턴스에 접근한 후 데이터를 수정하는 방식입니다. 이때 객체의 반응형을 유지하기 위해 toRefs를 활용하는 것이 핵심입니다.
자식 컴포넌트 (UserProfile.vue)
<template>
<div>이름: {{ profile.name }}</div>
<div>나이: {{ profile.age }}</div>
<div>도시: {{ profile.address.city }}</div>
<div>우편번호: {{ profile.address.zipCode }}</div>
<div>총 멤버 수: {{ memberCount }}</div>
</template>
<script setup>
import { reactive, ref, toRefs } from 'vue';
const profile = reactive({
name: '홍길동',
age: 28,
address: {
city: '서울',
zipCode: '04524'
}
});
const memberCount = ref(100);
// 반응형 객체를 전개할 때는 toRefs를 사용해야 부모에서 수정 시 동기화됨
defineExpose({
...toRefs(profile),
memberCount
});
</script>
부모 컴포넌트
부모 컴포넌트에서는 onMounted 훅을 사용하여 자식 컴포넌트가 마운트된 후 노출된 데이터에 접근합니다. 단순히 구조 분해 할당을 사용하면 반응형 연결이 끊기므로, toRefs를 사용하여 반응형 참조를 유지해야 합니다.
<template>
<UserProfile ref="profileRef" />
</template>
<script setup>
import { ref, onMounted, toRefs } from 'vue';
import UserProfile from './UserProfile.vue';
const profileRef = ref(null);
onMounted(() => {
setTimeout(() => {
// 자식 컴포넌트의 노출된 인스턴스 가져오기
const exposedInstance = profileRef.value;
// toRefs를 사용하여 반응형 참조로 변환 (단순 전개는 읽기 전용이 되거나 반응형이 끊김)
const reactiveData = { ...toRefs(exposedInstance) };
// 값 수정 시 자식 컴포넌트의 UI에 즉각 반영됨
reactiveData.memberCount.value = 150;
reactiveData.name.value = '김철수';
}, 3000);
});
</script>
패턴 2: 사용자 정의 이벤트(emit)를 통한 참조 전달
자식 컴포넌트가 자신의 반응형 객체 참조를 사용자 정의 이벤트를 통해 부모로 전달하는 방식입니다. 부모는 전달받은 참조를 통해 직접 객체의 속성을 수정할 수 있으며, 이는 자식 컴포넌트의 화면에 즉각적으로 반영됩니다. 이 방식은 getCurrentInstance를 사용하는 것보다 defineEmits의 반환 값을 활용하는 것이 더 권장되는 모범 사례입니다.
자식 컴포넌트 (ProductInfo.vue)
<template>
<div>상품명: {{ item.title }}</div>
<div>가격: {{ item.price }}</div>
<button @click="shareItemData">부모에게 상품 정보 전달</button>
</template>
<script setup>
import { reactive } from 'vue';
const item = reactive({
title: '무선 마우스',
price: 25000,
stock: 50
});
// defineEmits를 통해 이벤트 발생 함수를 직접 할당
const emit = defineEmits(['updateItem']);
const shareItemData = () => {
// 반응형 객체의 참조를 부모로 전달
emit('updateItem', item);
};
</script>
부모 컴포넌트
<template>
<ProductInfo @updateItem="handleItemUpdate" />
</template>
<script setup>
import ProductInfo from './ProductInfo.vue';
const handleItemUpdate = (data) => {
// 전달받은 반응형 객체의 참조를 직접 수정
data.title = '기계식 키보드';
data.price = 89000;
};
</script>