템플릿 ref로 자식 메서드를 부르려는데 undefined였던 이유
문제 발생
부모에서 자식 컴포넌트의 메서드를 부르려고 템플릿 ref를 잡았는데 undefined였습니다.
<!-- 부모 -->
<script setup>
const modal = ref(null);
function open() {
modal.value.show(); // TypeError: modal.value.show is not a function
}
</script>
<template><Modal ref="modal" /></template>자식에는 분명히 show()가 있었습니다.
<!-- Modal.vue -->
<script setup>
function show() { ... }
</script>원인 분석
<script setup>을 쓰는 컴포넌트는 기본적으로 닫혀 있습니다. 공식 문서의 표현 그대로 — 템플릿 ref나 $parent 체인으로 얻는 컴포넌트의 공개 인스턴스는 <script setup> 안에서 선언한 바인딩을 전혀 노출하지 않습니다.
이건 버그가 아니라 의도된 캡슐화입니다. <script setup> 이전의 Options API에서는 methods에 선언한 것이 전부 밖에서 보였고, 그래서 컴포넌트의 "공개 API"와 내부 구현이 구분되지 않았습니다. 지금은 밖에서 쓸 것만 명시적으로 열어야 합니다.
여는 도구가 defineExpose 컴파일러 매크로입니다.
해결 방안
- 밖에서 쓸 것만 명시적으로 노출합니다.
<!-- Modal.vue -->
<script setup>
import { ref } from "vue";
const isOpen = ref(false);
function show() { isOpen.value = true; }
function hide() { isOpen.value = false; }
defineExpose({ show, hide });
</script>이제 부모의 modal.value.show()가 동작합니다. isOpen은 여전히 밖에서 보이지 않습니다 — 그게 목적입니다.
- ref는 자동으로 언랩됩니다. 노출한 객체 안의 ref는 일반 인스턴스에서처럼 자동 언랩되므로, 부모에서
.value를 붙이지 않습니다.
defineExpose({ count }); // 자식: ref
console.log(modal.value.count); // 부모: 숫자- 템플릿 ref는 마운트 이후에 채워집니다. setup 본문에서 바로 읽으면
null입니다.
const modal = ref(null);
onMounted(() => {
console.log(modal.value); // 여기서는 인스턴스
});- 부를 수 있다고 해서 항상 부르지는 않습니다. 부모가 자식의 메서드를 직접 호출하는 것은 결합을 만듭니다. 대부분의 경우 상태를 내려보내는 편이 낫습니다.
<Modal v-model:open="isModalOpen" />명령형 호출이 맞는 경우는 상태로 표현하기 어색한 동작들입니다 — 포커스 이동, 스크롤, 파일 선택창 열기, 애니메이션 재생 같은 것들.
-
<script setup>이 아니면 이 문제가 없습니다. 일반setup()에서 반환한 값이나 Options API의methods는 그대로 노출됩니다. 그래서 예제를 복사해 올 때 두 방식이 다르게 동작하는 것처럼 보입니다. -
타입도 같이 열립니다. TypeScript에서는
InstanceType으로 노출된 형태를 받을 수 있어,show()가 있는지 컴파일 시점에 확인됩니다.
const modal = ref<InstanceType<typeof Modal> | null>(null);
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.