v-for로 만든 ref 배열의 3번째가 화면의 3번째가 아니었던 이유
문제 발생
목록의 특정 행으로 스크롤하려고 ref를 모았습니다.
<template>
<li v-for="item in items" ref="rows">{{ item.name }}</li>
</template>
<script setup>
const rows = useTemplateRef("rows");
function scrollTo(index) {
rows.value[index].scrollIntoView(); // 가끔 엉뚱한 행으로 이동
}
</script>처음 렌더에서는 잘 되는데, 목록을 필터링하거나 정렬한 뒤에는 다른 행으로 스크롤됐습니다.
원인 분석
인덱스가 대응한다는 보장이 없습니다. Vue 문서가 이 점을 명시적으로 적어둡니다 — v-for 안에서 ref를 쓰면 해당 ref는 마운트 후 요소들로 채워지는 배열이 되지만, 그 ref 배열이 원본 배열과 같은 순서를 보장하지는 않습니다.
이유는 렌더러가 DOM을 패치하기 때문입니다. 목록이 바뀌면 요소를 전부 새로 만들지 않고 재사용·이동하며, ref 등록/해제 순서는 그 패치 순서를 따릅니다. 그래서 rows.value[2]가 화면의 세 번째 항목이라는 보장이 사라집니다.
여기에 흔한 실수가 하나 더 겹칩니다. 문서 설명대로 템플릿 ref는 컴포넌트가 마운트된 뒤에만 접근할 수 있고, 첫 렌더에서는 null입니다.
해결 방안
- 키로 직접 대응시킵니다. 문서가 소개하는 함수 ref를 쓰면 어떤 요소가 어떤 데이터인지 우리가 정합니다.
<script setup>
const rowEls = new Map();
const setRow = (id) => (el) => {
if (el) rowEls.set(id, el);
else rowEls.delete(id); // 언마운트 시 인자는 null이다
};
function scrollTo(id) {
rowEls.get(id)?.scrollIntoView();
}
</script>
<template>
<li v-for="item in items" :key="item.id" :ref="setRow(item.id)">{{ item.name }}</li>
</template>-
애초에 DOM을 직접 만지지 않아도 되는지 봅니다. "선택된 행으로 스크롤"은
scroll-margin-top이나:target, 혹은 자식 컴포넌트에scrollIntoView를 노출하는 방식으로 풀리는 경우가 많습니다. -
접근 시점을 지킵니다. 값이 필요한 코드는
onMounted이후, 데이터가 바뀐 직후라면nextTick()다음에 둡니다. -
ref정리를 잊지 않습니다. 함수 ref는 요소가 사라질 때null로 호출되므로 그때 Map에서 지웁니다. 안 지우면 DOM에서 떨어진 노드를 계속 붙들고 있게 됩니다. -
key를 정확히 줍니다. 인덱스를key로 쓰면 요소 재사용이 뒤엉켜 이 문제가 더 자주 드러납니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.