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

v-for로 만든 ref 배열의 3번째가 화면의 3번째가 아니었던 이유

  • #Debugging
  • #Engineering Note
  • #Vue

문제 발생

목록의 특정 행으로 스크롤하려고 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입니다.

해결 방안

  1. 키로 직접 대응시킵니다. 문서가 소개하는 함수 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>
  1. 애초에 DOM을 직접 만지지 않아도 되는지 봅니다. "선택된 행으로 스크롤"은 scroll-margin-top이나 :target, 혹은 자식 컴포넌트에 scrollIntoView를 노출하는 방식으로 풀리는 경우가 많습니다.

  2. 접근 시점을 지킵니다. 값이 필요한 코드는 onMounted 이후, 데이터가 바뀐 직후라면 nextTick() 다음에 둡니다.

  3. ref 정리를 잊지 않습니다. 함수 ref는 요소가 사라질 때 null로 호출되므로 그때 Map에서 지웁니다. 안 지우면 DOM에서 떨어진 노드를 계속 붙들고 있게 됩니다.

  4. key를 정확히 줍니다. 인덱스를 key로 쓰면 요소 재사용이 뒤엉켜 이 문제가 더 자주 드러납니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

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