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

목록 순서를 바꿨더니 입력 중이던 값이 다른 행으로 옮겨간 이유

  • #Common Pitfall
  • #Engineering Note
  • #Vue

문제 발생

체크박스가 있는 목록에서 정렬 순서를 바꿨더니, 체크 상태가 엉뚱한 행에 남았습니다.

<li v-for="(todo, index) in sortedTodos" :key="index">
  <input type="checkbox" />
  {{ todo.name }}
</li>

두 번째 행을 체크한 뒤 정렬을 바꾸면, 내용은 바뀌었는데 체크는 여전히 두 번째 행에 있었습니다.

원인 분석

v-for에 키를 주지 않으면 Vue는 제자리 패치(in-place patch) 전략을 씁니다. 공식 문서의 설명대로, 데이터 순서가 바뀌어도 DOM 요소를 옮기지 않고 각 위치의 요소를 그 자리에서 갱신해 해당 인덱스에 와야 할 내용을 반영합니다.

빠르지만 전제가 있습니다. 문서는 이 전략이 렌더 결과가 자식 컴포넌트 상태나 임시 DOM 상태(폼 입력값 등)에 의존하지 않을 때만 적합하다고 명시합니다. 체크박스의 체크 여부는 정확히 그 "임시 DOM 상태"입니다. 요소가 재사용되면서 체크만 남은 것입니다.

여기서 :key="index"가 왜 도움이 안 되는지가 핵심입니다. 키는 각 노드의 정체성을 알려주기 위한 것인데, 인덱스는 데이터가 아니라 위치입니다. 순서가 바뀌면 인덱스도 함께 바뀌므로 "2번 자리에 있던 것은 계속 2번"이라는 정보밖에 주지 못합니다. 결과적으로 키가 없을 때의 제자리 패치와 같아집니다.

키에 쓸 값에도 제약이 있습니다. 문서가 못박습니다 — 키 바인딩은 문자열과 숫자 같은 원시 값을 기대합니다. 객체를 v-for 키로 쓰지 마십시오.

해결 방안

  1. 데이터 자체의 고유 값을 키로 씁니다.
<li v-for="todo in sortedTodos" :key="todo.id">
  <input type="checkbox" />
  {{ todo.name }}
</li>
  1. 인덱스를 키로 쓰는 것은 예외적인 경우로 둡니다. 순서가 절대 바뀌지 않고 항목이 중간에 추가·삭제되지 않으며 내부에 상태가 없는 목록이라면 문제되지 않습니다. 그 외에는 고유 id를 만들어 붙이는 편이 낫습니다.

  2. 서버 데이터에 id가 없으면 만들어 둡니다. 렌더링 때마다 새로 만들면 매번 다른 키가 되어 전부 다시 그려집니다. 데이터를 받는 시점에 한 번만 붙입니다.

const todos = ref(
  raw.map((item) => ({ ...item, key: `${item.title}-${item.createdAt}` })),
);
  1. v-ifv-for를 같은 요소에 쓰지 않습니다. 문서 기준 v-ifv-for보다 우선순위가 높기 때문에, v-if 조건은 v-for의 스코프 변수에 접근할 수 없습니다.
<!-- ❌ todo가 정의되지 않았다는 오류 -->
<li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li>

<template>으로 감싸면 해결됩니다.

<template v-for="todo in todos" :key="todo.id">
  <li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
  1. 필터링은 computed에서 미리 합니다. 템플릿이 단순해지고 조건 계산이 캐시됩니다.
const activeTodos = computed(() => todos.value.filter((todo) => !todo.isComplete));

공식 문서

마지막 수정

좋아요북마크

댓글0

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