본문으로 건너뛰기
개발 머꼬
개발 노트Vue
hohyeon.dev19

템플릿 ref로 자식 메서드를 부르려는데 undefined였던 이유

  • #Common Pitfall
  • #Engineering Note
  • #Vue

문제 발생

부모에서 자식 컴포넌트의 메서드를 부르려고 템플릿 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 컴파일러 매크로입니다.

해결 방안

  1. 밖에서 쓸 것만 명시적으로 노출합니다.
<!-- 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은 여전히 밖에서 보이지 않습니다 — 그게 목적입니다.

  1. ref는 자동으로 언랩됩니다. 노출한 객체 안의 ref는 일반 인스턴스에서처럼 자동 언랩되므로, 부모에서 .value를 붙이지 않습니다.
defineExpose({ count });        // 자식: ref
console.log(modal.value.count); // 부모: 숫자
  1. 템플릿 ref는 마운트 이후에 채워집니다. setup 본문에서 바로 읽으면 null입니다.
const modal = ref(null);
onMounted(() => {
  console.log(modal.value);   // 여기서는 인스턴스
});
  1. 부를 수 있다고 해서 항상 부르지는 않습니다. 부모가 자식의 메서드를 직접 호출하는 것은 결합을 만듭니다. 대부분의 경우 상태를 내려보내는 편이 낫습니다.
<Modal v-model:open="isModalOpen" />

명령형 호출이 맞는 경우는 상태로 표현하기 어색한 동작들입니다 — 포커스 이동, 스크롤, 파일 선택창 열기, 애니메이션 재생 같은 것들.

  1. <script setup>이 아니면 이 문제가 없습니다. 일반 setup()에서 반환한 값이나 Options API의 methods는 그대로 노출됩니다. 그래서 예제를 복사해 올 때 두 방식이 다르게 동작하는 것처럼 보입니다.

  2. 타입도 같이 열립니다. TypeScript에서는 InstanceType으로 노출된 형태를 받을 수 있어, show()가 있는지 컴파일 시점에 확인됩니다.

const modal = ref<InstanceType<typeof Modal> | null>(null);

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.