본문 바로가기
Javascript/Vue.js

[Vue.js] 리스트 렌더링과 Key

by dvid 2023. 5. 9.

리스트 렌더링

v-for

v-for 디렉티브를 사용하여 일반 배열, 객체 배열 등 리스트 렌더링을 할 수 있다.

<tr v-for="fruit in fruits">
  {{ fruit }}
</tr>

숫자 범위에 활용

<span v-for="i in 10">
    {{ i }}
</span>

v-if와 v-for

v-ifv-for를 함께 사용할 수 있지만 권장되지 않는다.
v-ifv-for보다 우선순위가 높기 때문에 v-if에서 v-for변수에 접근할 수 없다.

// bad practice
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

// good practice
<li v-for="todo in todos">
  <span v-if="!todo.isComplete"> 
    {{ todo.name }}
  </span>
</li>

key를 통한 상태유지

Vue가 v-for로 렌더링 된 리스트를 업데이트할 때, 기본적으로 in-place patch 전략을 사용한다.
리스트 아이템의 순서가 변경된 경우, 아이템의 순서와 일치하도록 DOM 엘리먼트를 이동하는 대신, 변경이 필요한 인덱스의 엘리먼트들을 제자리에서 패치(patch)해 아이템을 렌더링 하도록 한다.

이런 기본 동작은 효율적이지만, 리스트 렌더링 출력이 자식 컴포넌트 상태 또는 임시 DOM 상태 (ex: 양식 입력 값)에 의존하지 않는 경우에만 유효하다.

This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).

!(자식 컴포넌트에 의존) && 임시 DOM 상태

Temporary DOM

Default

<template>
<div>
  <h1>v-bind:key="None"</h1>
  <table>
    <thead>
      <tr>
        <th>None</th>
        <th>Name</th>
        <th>Value</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="fruit in fruits0"> <!-- eslint 빨간줄 -->
        <td> - </td>
        <td>{{ fruit.name }}</td>
        <td>
          <input type="number" />
        </td>
      </tr>
    </tbody>
  </table>
  <button v-on:click="shift0">shift</button>
</div>
<template>

<script setup>
  import { ref } from "vue";

  const fruits0 = ref([
    { id: 1, name: "apple" },
    { id: 2, name: "orange" },
    { id: 3, name: "melon" },
    { id: 4, name: "grape" },
    { id: 5, name: "mango" }
  ]);

  const shift0 = () => fruits0.value.push(fruits0.value.shift());
</script>
Before After

Array index key

<template>
<div>
  <h1>v-bind:key="Array Index"</h1>
  <table>
    <thead>
      <tr>
        <th>Index</th>
        <th>Name</th>
        <th>Value</th>
        <th>Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(fruit, i) in fruits1" :key="i">
        <td>{{ i }}</td>
        <td>{{ fruit.name }}</td>
        <td>
          <input type="number" />
        </td>
        <td>
          <button @click="deleteFruit2(fruit)">X</button>
        </td>
      </tr>
    </tbody>
  </table>
  <button v-on:click="shift1">shift</button>
</div>
</template>
<script setup>
  import { ref } from "vue";

  const fruits1 = ref([
    { id: 1, name: "apple" },
    { id: 2, name: "orange" },
    { id: 3, name: "melon" },
    { id: 4, name: "grape" },
    { id: 5, name: "mango" }
  ]);

  const shift1 = () => fruits1.value.push(fruits1.value.shift());
  const deleteFruit1 = (fruit) => (fruits1.value = fruits1.value.filter((f) => f.id !== fruit.id))
  const deleteFruit2 = (fruit) => (fruits2.value = fruits2.value.filter((f) => f.id !== fruit.id));
</script>

수정

Before After

0번 인덱스에서 fruit 객체가 shift되지만, 다시 0번 인덱스가 바로 채워진다
또한, 4번 인덱스가 3번으로 당겨지지만, 바로 4번 인덱스에 값이 채워진다.
따라서 키가 변하지 않으므로 tr 객체에는 변화가 없다. 그래서 input 태그는 변화가 없고, 내부의 {{ fruit.name }}만 변경된다.

삭제

Before After
none-before none-after
  • 해당 아이템의 key(인덱스)가 변경되지 않고, 인덱스 순서에 맞게 변경된다.
  • 아이템 삭제 시 객체의 key인 인덱스가 함께 변경되므로 index key는 사용하지 않는 것이 좋다.

Object Id key

<template>
<div>
  <h1>v-bind:key="Object Id"</h1>
  <table>
    <thead>
      <tr>
        <th>Object Id</th>
        <th>Name</th>
        <th>Value</th>
        <th>Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="fruit in fruits2" :key="fruit.id">
        <td>{{ fruit.id }}</td>
        <td>{{ fruit.name }}</td>
        <td><input type="number" /></td>
        <td>
          <button @click="deleteFruit2(fruit)">X</button>
        </td>
      </tr>
    </tbody>
  </table>
  <button v-on:click="shift2">shift</button>
</div>
</template>
<script setup>
  import { ref } from "vue";

  const fruits2 = ref([
    { id: 1, name: "apple" },
    { id: 2, name: "orange" },
    { id: 3, name: "melon" },
    { id: 4, name: "grape" },
    { id: 5, name: "mango" }
  ]);

  const shift2 = () => fruits2.value.push(fruits2.value.shift());

  const deleteFruit2 = (fruit) => (fruits2.value = fruits2.value.filter((f) => f.id !== fruit.id))
</script>

수정

fruit.id라는 고유값을 가진 tr 태그가 shift 후 다른 fruit.id를 가진 tr 태그로 채워지므로 tr 태그 자체가 다른 값으로 바뀌었다.
따라서 input태그 또한 함께 이동하였다.

Before After

삭제

Before After
none-before none-after
  • 객체의 key가 함께 변경되었다.

결론

반복되는 DOM 컨텐츠가 단순하거나, 의도적으로 렌더링 동작을 통해 성능 향상을 꾀하는 경우가 아니라면, 가능한 v-forkey 속성과 함께 사용하는 것을 권장한다.

key: number | string | symbol

애니메이션의 객체 불변성

배열 변경 감지

수정 메서드

  • Vue는 반응형 배열의 메서드가 호출되는 것을 감지하여, 필요한 업데이트를 발생시킵니다.

push() pop() shift() unshift() splice() sort() reverse()

배열 교체

  • filter() concat() slice()는 원본 배열을 수정하지 않고 새 배열을 반환하므로 기존의 배열(ref([]). value)을 변경해야 한다.

출처

댓글