Vue 3 script setup 환경에서 부모 컴포넌트가 자식 데이터에 접근하고 수정하는 패턴

Vue 3의 <script setup> 구문에서는 컴포넌트 인스턴스가 기본적으로 닫혀 있어 부모가 자식의 내부 상태에 직접 접근할 수 없습니다. 하지만 아키텍처 요구사항에 따라 부모 컴포넌트가 자식 컴포넌트의 데이터를 제어해야 하는 상황이 발생할 수 있습니다. 이를 구현하는 두 가지 주요 기술적 패턴을 살펴봅니다.

패턴 1: defineExposeref를 활용한 직접 접근

자식 컴포넌트에서 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>

태그: vue3 CompositionAPI ScriptSetup DefineExpose DefineEmits

6월 6일 19:50에 게시됨