Template Refs
Template Ref는 Vue의 선언적 렌더링으로 해결할 수 없는, DOM을 직접 조작해야 하는 경우에 사용하는 탈출구(escape hatch)입니다.
면접에서 "Vue에서 DOM에 직접 접근해야 할 때 어떻게 하나요?"라고 물으면, ref 속성과 onMounted 타이밍을 함께 설명할 수 있어야 합니다.
기본 사용법
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 변수명이 template의 ref 속성값과 일치해야 함
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// onMounted 이후에 DOM 접근 가능
inputRef.value?.focus()
})
const focusInput = () => {
inputRef.value?.focus()
}
</script>
<template>
<!-- ref 속성으로 DOM 엘리먼트 참조 -->
<input ref="inputRef" placeholder="자동 포커스" />
<button @click="focusInput">포커스</button>
</template>
**주의 **: ref는 onMounted 이후에만 값이 채워집니다. setup() 실행 시점에는 null입니다.
v-for에서의 Ref
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const items = ref(['사과', '바나나', '오렌지'])
const itemRefs = ref<HTMLLIElement[]>([])
onMounted(() => {
// 배열로 모든 엘리먼트 참조를 받음
console.log(itemRefs.value.length) // 3
itemRefs.value.forEach((el, i) => {
console.log(`${i}: ${el.textContent}`)
})
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
** 주의 **: ref 배열의 순서가 소스 배열의 순서와 반드시 일치하지는 않습니다.
함수 Ref
더 세밀한 제어가 필요할 때 함수를 사용할 수 있습니다.
<script setup lang="ts">
import { ref } from 'vue'
const dynamicRefs = ref<Map<string, HTMLElement>>(new Map())
const setRef = (el: HTMLElement | null, key: string) => {
if (el) {
dynamicRefs.value.set(key, el)
} else {
dynamicRefs.value.delete(key)
}
}
const scrollTo = (key: string) => {
dynamicRefs.value.get(key)?.scrollIntoView({ behavior: 'smooth' })
}
</script>
<template>
<div v-for="section in ['intro', 'features', 'pricing']" :key="section">
<div :ref="(el) => setRef(el as HTMLElement, section)">
<h2>{{ section }}</h2>
</div>
</div>
<nav>
<button @click="scrollTo('intro')">소개</button>
<button @click="scrollTo('features')">기능</button>
<button @click="scrollTo('pricing')">가격</button>
</nav>
</template>
컴포넌트 Ref
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const message = ref('안녕하세요')
const increment = () => { count.value++ }
const reset = () => { count.value = 0 }
// script setup에서는 명시적으로 expose해야 외부에서 접근 가능
defineExpose({ count, increment, reset })
</script>
<template>
<div>{{ message }} — {{ count }}</div>
</template>
<!-- 부모 컴포넌트 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)
onMounted(() => {
// expose된 속성만 접근 가능
console.log(childRef.value?.count) // 0
childRef.value?.increment()
// childRef.value?.message // expose하지 않았으므로 접근 불가
})
</script>
<template>
<ChildComponent ref="childRef" />
<button @click="childRef?.reset()">리셋</button>
</template>
실전 활용 패턴
외부 라이브러리와 통합
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const canvasRef = ref<HTMLCanvasElement | null>(null)
let animationId: number
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// 캔버스 크기 설정
canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight
// 애니메이션 루프
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 그리기 로직...
animationId = requestAnimationFrame(animate)
}
animate()
})
onUnmounted(() => {
cancelAnimationFrame(animationId)
})
</script>
<template>
<canvas ref="canvasRef" style="width: 100%; height: 400px;"></canvas>
</template>
스크롤 위치 복원
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue'
const scrollContainerRef = ref<HTMLDivElement | null>(null)
let savedScrollTop = 0
// KeepAlive와 함께 사용 시 스크롤 위치 복원
onActivated(() => {
if (scrollContainerRef.value) {
scrollContainerRef.value.scrollTop = savedScrollTop
}
})
const saveScrollPosition = () => {
if (scrollContainerRef.value) {
savedScrollTop = scrollContainerRef.value.scrollTop
}
}
</script>
<template>
<div ref="scrollContainerRef" @scroll="saveScrollPosition" style="overflow-y: auto; height: 500px;">
<!-- 긴 컨텐츠 -->
</div>
</template>
포커스 트래핑 (모달)
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
const modalRef = ref<HTMLDivElement | null>(null)
const isOpen = ref(false)
const open = async () => {
isOpen.value = true
// DOM 업데이트를 기다린 후 포커스 설정
await nextTick()
const firstFocusable = modalRef.value?.querySelector<HTMLElement>(
'button, [href], input, select, textarea'
)
firstFocusable?.focus()
}
</script>
<template>
<button @click="open">모달 열기</button>
<div v-if="isOpen" ref="modalRef" role="dialog" aria-modal="true">
<h2>모달 제목</h2>
<input placeholder="입력..." />
<button @click="isOpen = false">닫기</button>
</div>
</template>
nextTick과 함께 사용
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const showInput = ref(false)
const dynamicInputRef = ref<HTMLInputElement | null>(null)
const toggleAndFocus = async () => {
showInput.value = true
// v-if로 인해 DOM이 아직 없으므로 nextTick 필요
await nextTick()
dynamicInputRef.value?.focus()
}
</script>
<template>
<button @click="toggleAndFocus">입력 필드 표시</button>
<input v-if="showInput" ref="dynamicInputRef" />
</template>
면접 팁
- Template Ref는 ** 탈출구 **입니다. 가능하면 선언적 바인딩을 사용하고, DOM 직접 접근이 불가피할 때만 사용하세요
defineExpose를 사용해야 하는 이유를 ** 캡슐화 **와 연결지어 설명하면 좋습니다nextTick의 존재 이유를 Vue의 비동기 DOM 업데이트 큐 와 연결해서 설명할 수 있으면 깊이 있어 보입니다
요약
Template Ref는 ref 속성으로 DOM 엘리먼트나 컴포넌트 인스턴스에 직접 접근하는 방법입니다. onMounted 이후에만 사용 가능하고, nextTick으로 DOM 업데이트를 기다려야 할 때가 있습니다. 컴포넌트 ref는 defineExpose로 공개된 속성만 접근 가능합니다.
댓글 로딩 중...