Vue 3 Composition API에서 컴포넌트 기능 노출하기
Olivia Novak
Dev Intern · Leapcell

소개
현대 프런트엔드 개발의 활발한 생태계에서 컴포넌트 기반 아키텍처는 확장 가능하고 유지보수 가능한 애플리케이션 구축의 초석이 되었습니다. Vue 3는 강력한 Composition API를 통해 컴포넌트 로직을 구성하고 관리하는 방식을 혁신하여 재사용성과 명시적인 상태 관리를 선호합니다. 그러나 부모가 자식 컴포넌트의 내부 상태나 메서드와 상호 작용해야 하는 일반적인 시나리오가 발생합니다. Props와 이벤트가 대부분의 부모-자식 통신을 처리하지만, 부모가 자식의 명령형 API나 내부 상태에 직접 액세스해야 하는 특정 경우가 있습니다. 바로 이때 defineExpose가 개입하여 기본 캡슐화를 해제하고 컴포넌트 내부의 특정 측면을 노출하는 제어되고 의도적인 방법을 제공합니다. 특히 복잡한 컴포넌트 상호 작용을 다루거나 매우 재사용 가능한 UI 라이브러리를 구축할 때, 그 목적과 올바른 사용법을 이해하는 것은 강력하고 유연한 Vue 애플리케이션을 작성하는 데 중요합니다.
컴포넌트 캡슐화 및 defineExpose 이해하기
defineExpose에 대해 자세히 알아보기 전에 Vue 컴포넌트의 기본 캡슐화에 대해 간략히 살펴보겠습니다. 설계상 컴포넌트의 내부 상태(변수, 반응형 데이터, 계산된 속성) 및 <script setup> 블록 내에 정의된 메서드는 비공개입니다. Props를 통해 명시적으로 전달되거나 이벤트를 통해 방출되지 않는 한 부모 컴포넌트에서 직접 액세스할 수 없습니다. 이러한 캡슐화는 모듈성을 촉진하고 의도하지 않은 부작용을 방지하는 좋은 실천 방법입니다.
하지만 때로는 이러한 엄격한 캡슐화가 방해가 될 수 있습니다. 명령형 focus() 메서드가 필요한 사용자 정의 폼 입력 컴포넌트나 부모에게 open() 및 close() 메서드를 노출해야 하는 모달 컴포넌트를 구축한다고 상상해 보세요. 이러한 시나리오에서는 Props와 이벤트가 번거롭거나 덜 직관적일 수 있습니다. 바로 이때 defineExpose가 격차를 해소합니다.
핵심 용어
- Composition API: Vue 3의 API 세트로, 가져온 함수를 사용하여 컴포넌트 로직을 구성할 수 있습니다. Options API에 비해 코드를 구성하고 재사용할 수 있는 더 유연하고 강력한 방법을 제공합니다.
 setup스크립트: Vue 단일 파일 컴포넌트(SFC) 내 Composition API 로직의 기본 진입점입니다. 모든 반응형 상태, 계산된 속성, 메서드 및 수명 주기 훅은 일반적으로 여기에 정의됩니다.ref: 값을 보유하는 반응형 참조입니다. 이를 통해 기본값(문자열, 숫자, 부울) 및 개체의 변경 사항을 추적할 수 있습니다.provide/inject: Prop 드릴링 방지 메커니즘으로, 각 수준에서 Props를 수동으로 전달하지 않고도 컴포넌트 트리를 통해 데이터를 아래로 전달할 수 있습니다. 데이터 흐름과 관련이 있지만defineExpose와는 다른 목적을 제공합니다.- 컴포넌트 인스턴스: DOM에 마운트된 컴포넌트를 나타내는 실제 JavaScript 객체입니다. 부모 컴포넌트는 템플릿 ref를 통해 자식 컴포넌트 인스턴스에 대한 참조를 얻을 수 있습니다.
 - 템플릿 Ref: 템플릿의 컴포넌트 또는 HTML 요소에 있는 
ref="myRef"속성으로, 스크립트에서 기본 DOM 요소 또는 컴포넌트 인스턴스에 직접 액세스할 수 있는 방법을 제공합니다. 
defineExpose의 역할
defineExpose는 <script setup> 내에서 사용할 수 있는 컴파일러 매크로로, 템플릿 ref를 통해 컴포넌트 인스턴스에 액세스할 때 어떤 속성과 메서드를 노출할지 명시적으로 정의할 수 있습니다. defineExpose 없이 템플릿 ref를 통해 컴포넌트 인스턴스에 액세스하면 빈 객체 또는 Vue 내부 컴포넌트 인스턴스에서 암시적으로 상속된 속성만 얻게 됩니다. defineExpose를 사용하면 외부 컴포넌트가 볼 수 있고 상호 작용할 수 있는 것을 정밀하게 제어하여 컴포넌트 내부의 공개 인터페이스를 효과적으로 생성할 수 있습니다.
defineExpose 사용 방법
간단한 카운터 컴포넌트 예제를 통해 설명하겠습니다.
<!-- MyCounter.vue --> <script setup> import { ref, computed } from 'vue'; const count = ref(0); const doubleCount = computed(() => count.value * 2); const increment = () => { count.value++; }; const decrement = () => { count.value--; }; // 'count'와 'increment'를 부모에게 노출 defineExpose({ count, increment, // 참고: doubleCount와 decrement는 기본적으로 노출되지 않습니다. // doubleCount를 노출하려면 여기에 추가하세요: // doubleCount }); </script> <template> <div> <p>Count: {{ count }}</p> <p>Double Count: {{ doubleCount }}</p> <button @click="decrement">Decrypt</button> </div> </template>
이제 부모 컴포넌트가 템플릿 ref를 사용하여 MyCounter와 상호 작용하는 방법을 살펴보겠습니다.
<!-- ParentComponent.vue --> <script setup> import { ref, onMounted } from 'vue'; import MyCounter from './MyCounter.vue'; const counterRef = ref(null); onMounted(() => { if (counterRef.value) { console.log("Initial count from child:", counterRef.value.count.value); // 노출된 ref에 액세스 counterRef.value.increment(); // 노출된 메서드 호출 console.log("Count after increment:", counterRef.value.count.value); // console.log("Double count:", counterRef.value.doubleCount); // 이 값은 undefined일 것입니다. // counterRef.value.decrement(); // 이 값은 undefined일 것입니다. } }); const handleParentIncrement = () => { if (counterRef.value) { counterRef.value.increment(); } }; </script> <template> <div> <h1>Parent Component</h1> <MyCounter ref="counterRef" /> <button @click="handleParentIncrement">Increment from Parent</button> </div> </template>
ParentComponent.vue에서는 MyCounter 컴포넌트에 ref="counterRef"를 사용합니다. onMounted 훅에서 counterRef.value는 이제 MyCounter.vue에서 defineExpose를 사용하여 명시적으로 노출한 속성을 가진 객체를 포함합니다. 그런 다음 counterRef.value.count에 직접 액세스하고 counterRef.value.increment()를 호출할 수 있습니다. doubleCount와 decrement는 명시적으로 노출되지 않았기 때문에 액세스할 수 없다는 점에 유의하세요.
defineExpose를 사용해야 할 때
defineExpose는 신중하게 사용해야 합니다. 과도하게 사용하면 캡슐화를 방해하고 컴포넌트를 이해하고 리팩터링하기 어렵게 만들 수 있습니다. defineExpose가 매우 유익한 일반적인 시나리오는 다음과 같습니다.
- 명령형 API 제공: 직접적인 메서드를 제공해야 하는 컴포넌트의 경우:
play(),pause(),seek()가 있는 사용자 정의 비디오 플레이어.open(),close()가 있는 모달 또는 대화 상자 컴포넌트.focus(),reset(),validate()메서드가 필요한 폼 요소.
 - 내부 상태 노출(주의): 부모가 실제로 자식의 내부 상태의 특정 부분을 관찰하거나 조작해야 할 때. 이는 메서드 노출보다 덜 일반적이며 명확한 근거를 가지고 수행해야 합니다. 예:
- 외부 지속성을 위해 현재 정렬 또는 필터링 상태를 노출하는 정교한 데이터 테이블 컴포넌트.
 - 부모가 현재 애니메이션 진행률을 읽어야 하는 복잡한 애니메이션 컴포넌트.
 
 - 타사 통합: 비 Vue 라이브러리 또는 DOM 요소를 래핑하고 해당 특정 메서드 또는 속성을 부모에게 노출해야 하는 경우.
 - 재사용 가능한 UI 라이브러리 구축: 광범위한 사용을 위해 설계된 컴포넌트의 경우, 
defineExpose를 통해 잘 정의된 공개 API를 제공하면 라이브러리 소비자가 제어되고 예측 가능한 방식으로 컴포넌트와 상호 작용할 수 있습니다. 
고려 사항 및 모범 사례
- Props 및 이벤트 우선: 항상 부모-자식 데이터 흐름에는 Props를, 자식-부모 통신에는 이벤트를 기본으로 사용하세요. 
defineExpose는 이 규칙의 예외를 위한 것입니다. - 명시적인 것이 암시적인 것보다 낫습니다: 
defineExpose는 무엇이 액세스 가능한지 명시하도록 강제하여 내부 구현 세부 정보가 실수로 노출되는 것을 방지합니다. - 노출된 API 최소화: 외부 제어 또는 관찰에 정말 필요한 것만 노출하세요. 적게 노출할수록 컴포넌트를 유지 관리하고 리팩터링하기가 더 쉬워집니다.
 - 문서화: 재사용 가능한 컴포넌트를 구축하는 경우 jsdoc 주석이나 컴포넌트 설명서에 노출된 속성과 메서드를 명확하게 문서화하세요.
 - 타입 안전성(TypeScript): TypeScript를 사용할 때 더 나은 개발자 경험과 컴파일 타임 검사를 위해 노출된 속성에 타입을 지정할 수도 있지만, 컴포넌트의 노출된 인터페이스를 정의하는 약간 더 고급 패턴이 필요합니다.
 
결론
defineExpose는 Vue 3 Composition API에서 강력하고 필수적인 도구로, 개발자가 컴포넌트 캡슐화를 선택적으로 해제하고 내부 상태 또는 메서드를 노출할 수 있도록 합니다. 컴포넌트 간 상호 작용의 기본 통신 채널(Props 및 이벤트)은 여전히 주요 수단이지만, defineExpose는 직접적인 명령형 제어 또는 액세스가 필요한 시나리오에 중요한 탈출구를 제공합니다. 이를 신중하고 사려 깊게 사용하고, 적절한 캡슐화를 우선시하며, 명확한 공개 인터페이스를 유지함으로써 복잡한 상호 작용을 우아하게 처리하는 더 유연하고 재사용 가능하며 강력한 Vue 컴포넌트를 구축할 수 있습니다. 궁극적으로 이는 컴포넌트 간의 정확한 계약을 작성할 수 있도록 하여 기능과 유지 관리성을 모두 향상시킵니다.

