[Vue 빠른시작] List Rendering 예제 추가

[Vue 빠른시작] List Rendering 예제 추가

1. v-for

v-for 디렉티브를 사용하여 배열을 리스트로 렌더링 할 수 있다. v-for 디렉티브는 item in items 형식의 특별한 문법이 필요하다. 여기서 items는 배열이고, item(이하 아이템)은 배열 내 반복되는 엘리먼트의 별칭(alias) 이다.

// Options API ver
<script>
export default {
  data() {
  return {
    items: [{message: 'foo'}, {message: 'bar'}]
  }
}
}
</script>

<template>
  <li v-for="item in items">
    {{ item.message }}
  </li>
</template>
// Composition API ver
const items = ref([{ message: 'foo' }, { message: 'bar' }])

에러 발생 화면

docs에 나와있는 예제를 따라하면 실행은 되지만 위 에러가 등장한다.

[vue/require-v-for-key]
Elements in iteration expect to have 'v-bind:key' directives.eslint-plugin-vue

v-for의 기본 동작은 엘리먼트를 이동하지 않고 제자리에 패치(patch)하려고 한다. 강제로 엘리먼트를 재정렬하려면, 각 항목에 고유한 특수 속성 key를 사용하여 순서 지정을 위한 힌트(:key)를 제공해야 한다.


예제 수정

// Options API ver
<script>
export default {
  data() {
  return {
    items: [{id: 1, message: 'foo'}, {id: 2, message: 'bar'}]
  }
}
}
</script>

<template>
  <li v-for="item in items" :key="item.id">
    {{ item.message }}
  </li>
</template>

v-for 범위 내 템플릿 표현식은 모든 상위 범위 속성에 접근할 수 있다. 또한 v-for는 현재 아이템의 인덱스를 가리키는 선택적 두 번째 별칭도 지원한다.

// Options API ver
<script>
export default {
  data() {
  return {
    parentMessage: 'Parent',
    items: [{id: 1, message: 'foo'}, {id: 2, message: 'bar'}]
  }
}
}
</script>

<template>
  <li v-for="(item, index) in items" :key="item.id">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</template>

v-for의 변수 범위는 JavaScript와 유사하다.

// Options API ver
<script>
export default {
  data() {
    return {
      parentMessage: 'Parent',
      items: [{id: 1, message: 'foo'}, {id: 2, message: 'bar'}]
    }
  },
  methods: {
    example() {
      this.items.forEach((item, index) => {
        console.log(this.parentMessage, item.message, index);
      });
    }
  }
}
</script>

<template>
  <li v-for="(item, index) in items" :key="item.id">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
  <button @click="example()">여기</button>
</template>
// Composition API ver
const parentMessage = 'Parent'
const items = [ /* ... */ ]

items.forEach((item, index) => {
  // forEach의 콜백 함수 외부에 있는 'parentMessage'에 대한 접근 가능.
  // 반면 'item'과 'index'는 콜백함수 내부에서만 접근 가능.
  console.log(parentMessage, item.message, index)
})

실행화면

콜백 함수 인자를 분해 할당해 사용할 수 있는 것처럼, v-for의 아이템도 분해 할당해 사용할 수 있다.

// Options API ver
<script>
export default {
  data() {
    return {
      parentMessage: 'Parent',
      items: [{id: 1, message: 'foo'}, {id: 2, message: 'bar'}]
    }
  }
}
</script>

<template>
  <li v-for="{ message } in items" :key="message">
    {{ message }}
  </li>

  <!-- index 별칭도 사용 -->
  <li v-for="({ message }, index) in items" :key="index">
    {{ message }} {{ index }}
  </li>
</template>

중첩된 v-for의 경우, 중첩된 함수와 유사한 범위를 가진다. 각 v-for 범위에는 상위 범위에 대한 접근 권한이 있다.

// Options API ver
<script>
export default {
  data() {
    return {
      parentMessage: 'Parent',
      items: [
          {id: 1, message: 'foo', children: ['c-1-1', 'c-1-2']}, 
          {id: 2, message: 'bar', children: ['c-2-1', 'c-2-2']}
        ]
    }
  }
}
</script>

<template>
  <li v-for="item in items" :key="item">
    <span v-for="childItem in item.children" :key="childItem">
      {{ item.message }} {{ childItem }}
    </span>
  </li>
</template>

실행화면

in 대신 of를 구분 기호로 사용하여 JavaScript 반복문 문법 처럼 사용할 수도 있다.

<div v-for="item of items"></div>



2. 객체에 v-for 사용하기

v-for를 객체의 속성을 반복하는 데 사용할 수 있다. 순회 순서는 해당 객체를 Object.keys()를 호출한 결과에 기반한다.

// Options API ver
<script>
export default {
  data() {
    return {
      parentMessage: 'Parent',
      myObject: { 
        title: '점심에 감기약까지 먹고 난 뒤 오후 두시란... 내가 내가 아니야',
        author: 'feat.난방',
        publishedAt: '2024-01-15'
      }
    }
  }
}
</script>

<template>
  <li v-for="value in myObject" :key="value">
    {{ value }}
  </li>
</template>
// Composition API ver
const myObject = reactive({
  title: '...',
  author: '...',
  publishedAt: '...'
})

속성명을 가리키는 두 번째 별칭을 사용할 수도 있다.

<template>
  <li v-for="(value, key) in myObject" :key="value">
    {{ key }} : {{ value }}
  </li>

그리고 인덱스를 가리키는 세 번째 별칭을 사용할 수도 있다.

  <li v-for="(value, key, index) in myObject" :key="value">
    {{ index }} . {{ key }} : {{ value }}
  </li>



3. 숫자 범위에 v-for 사용하기

v-for는 정수를 사용할 수도 있다. 이 경우 1...n 범위를 기준으로 템플릿을 여러번 반복한다.

<span v-for="n in 10" :key="n">{{ n }}</span>

여기서 n의 값은 0이 아니라 1부터 시작한다.



4. <'template>에서 v-for 사용하기

v-if와 유사하게 <'template> 태그에 v-for를 사용하여 여러 엘리먼트 블록을 렌더링 할 수도 있다.

// Options API ver
<script>
export default {
  data() {
    return {
      items: [
        { id: 1, msg: 'Item 1' },
        { id: 2, msg: 'Item 2' }
      ]
    }
  }
}
</script>

<template>
  <ul>
    <template v-for="item in items" :key="item">
      <li>{{ item.msg }}</li>
      <li class="divider" role="presentation"></li>
    </template>
  </ul>
</template>

실행화면



5. v-if에 v-for 사용하기

note! v-if와 v-for를 함께 사용하는 것은 권장되지 않는다.

같은 노드에 존재할 때 v-if가 v-for보다 우선순위가 높기 때문에 v-if 조건문에서 v-for 변수에 접근할 수 없다.

오류 발생 예제

// Options API ver
<script>
export default {
  data() {
    return {
      todos: [
        { name: '청소하기', isComplete: false },
        { name: '도시락 반찬 싸놓기', isComplete: true },
        { name: '일찍 자기', isComplete: false }
      ]
    }
  }
}
</script>

<template>
  <li v-for="todo in todos" v-if="!todo.isComplete" :key="todo">
    {{ todo.name }}
  </li>
</template>

- 'todo' 속성이 '...' 형식에 없을 수 있습니다. 'todos'을(를) 사용하시겠습니까?ts(2568)
- The 'todos' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.eslint-plugin-vue

- [Vue warn]: Property "todo" was accessed during render but is not defined on instance. 
- [Vue warn]: Unhandled error during execution of render function 
- Uncaught TypeError: Cannot read properties of undefined (reading 'isComplete')

따라서 <'template> 태그로 감싼 후, v-for를 옮겨서 해결할 수 있다.(더 명시적이기도 함)

<template>
  <template v-for="todo in todos" :key="todo">
    <li v-if="!todo.isComplete">
      {{ todo.name }}
    </li>
  </template>
</template>



6. key를 통한 상태유지

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



note! in-place patch 전략?

패치가 어떻게 돌아가는지 함 짚고 넘어간다

예제

// Options API ver
<script>
export default {
  data() {
    return {
      items: [
        { id: 1, text: 'Item 1' },
        { id: 2, text: 'Item 2' },
        { id: 3, text: 'Item 3' },
      ],
    };
  },
  methods: {
    shuffleItems() {
      // 리스트의 아이템 순서를 무작위로 변경
      this.items = this.shuffleArray(this.items);
    },
    shuffleArray(array) {
      // Fisher-Yates 알고리즘을 사용하여 배열을 무작위로 섞음
      let currentIndex = array.length, randomIndex;
      while (currentIndex !== 0) {
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex--;

        // 비구조화 할당(구조 분해 할당)을 사용하여 배열의 두 요소를 서로 교환
        [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
        // 두 개의 요소로 이루어진 임시 배열 생성. = 각각 할당할 값
      }

      return array;
    },
  },
};
</script>

<template>
  <div>
    <ul>
      <li v-for="item in items">
        {{ item.text }} : <input type="number"/>
      </li>
    </ul>
    <button @click="shuffleItems">Shuffle Items</button>
  </div>
</template>

실행화면

🔺초기 상태

🔺셔플 버튼 동작 후

shuffleItems 메서드를 호출하면 items 배열의 아이템 순서가 무작위로 변경된다. 이때 'in-place patch'전략이 적용되어, 순서가 변경된 경우에도 Vue는 기존 DOM 엘리먼트를 이동시키지 않고 현재 위치에서 업데이트 한다.(셔플 버든 동작 후 input은 그대로 남아있고 변경된 item 정보면 패치 되었다.) 결과적으로 렌더링 성능이 최적화되어 부하를 최소화한다.




(key를 통한 상태유지 이어서...)

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

Vue가 각 노드의 ID를 추적하고 기존 엘리먼트를 재사용하고 재정렬 할 수 있도록 힌트를 제공하려면 각 항목에 대해 고유한 key 속성을 제공해야 한다.

    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.text }} : <input type="number"/>
      </li>
    </ul>

변경 후 실행 화면

<'template v-for>를 사용할 때 key는 <'template> 컨테이너에 있어야 한다.

 <template v-for="todo in todos" :key="todo.name">
    <li>{{ todo.name }}</li>
</template>



7. 컴포넌트에 v-for 사용하기

컴포넌트에 v-for를 직접 사용할 수 있다.

<MyDocument v-for="item in items" :key="item.id" />

그러나 컴포넌트에는 자체적으로 구분된 범위가 있기 때문에 컴포넌트에 데이터를 자동으로 전달하지 않는다. 반복된 데이터를 컴포넌트에 전달하려면 props를 사용해야 한다.

<MyDocument
    v-for="(item, index) in items" 
    :item="item"
    :index="index"
    :key="item.id" />

컴포넌트에 item이 자동으로 전달되지 않는 이유는, 그렇지 않으면 v-for를 사용해야만 컴포넌트 사용이 가능하도록 의존관계가 되기 때문이다. 반면, 데이터를 명시해서 전달하는 방법은 v-for를 사용하지 않는 다른 상황에서도 컴포넌트가 데이터 전달 기능을 재사용 할 수 있기 때문이다.



8. 배열 변경 감지

8-1. 수정 메서드

Vue는 반응형 배열의 변경 메소드가 호출 되는것을 감지하여, 필요한 업데이트를 발생시킨다. 지원하는 변경 메소드 목록은 다음과 같다.

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

예제

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </ul>
    <button @click="removeItem">Remove Last Item</button>
  </div>
</template>

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

// 반응형 배열 생성
const items = ref([
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' },
  { id: 3, text: 'Item 3' },
]);

// pop() 메소드를 사용하여 배열에서 마지막 항목 제거
const removeItem = () => {
  items.value.pop();
};
</script>

실행화면


🔺초기화면


🔺버튼 실행 후



8-2. 배열 교체

수정 메서드는 이름에서 알 수 있듯이 호출된 원래 배열을 수정한다. 이에 비해 수정이 아닌 방법도 있다. filter(), concat() 및 slice()는 원본 배열을 수정하지 않고 항상 새 배열을 반환한다. 이러한 방법으로 작업하는 경우, 이전 배열을 새 배열로 교체해야 한다.

// Options API ver
<script>
export default {
  data() {
    return {
      items: [
        { id: 1, text: 'Item 1' },
        { id: 2, text: 'Item 2' },
        { id: 3, text: 'Item 3' },
      ],
    };
  },
  methods: {
    filtering() {
      this.items = this.items.filter((item) => item.text.match(/1/))
    }
  },
};
</script>

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.text }}
      </li>
    </ul>
    <button @click="filtering">filtering Items</button>
  </div>
</template>

실행화면


🔺초기화면


🔺버튼 실행 후

이로 인해 Vue가 기존 DOM을 버리고 전체 리스트를 다시 렌더링할 것이라고 생각할 수도 있다. 그러나 Vue는 DOM 엘리먼트 재사용을 최대화하기 위해 몇가지 스마트 휴리스틱(smart heuristics)을 구현하므로, 이전 배열을 다른 배열로 바꾸는 과정에서 서로 중복되는 객체를 가지는 부분을 매우 효율적으로 처리한다.

note!

Smart heuristics 는 컴퓨터 과학이나 기계 학습 분야에서 사용되는 용어로, 문제 해결이나 의사 결정에 적용되는 지능적인 규칙 또는 전략을 말한다. 여기서 "heuristics(휴리스틱스)"는 문제를 해결하기 위한 규칙 또는 방법을 의미하며, "smart(스마트)"는 이 규칙이 특정 상황에서 효과적이거나 지능적으로 판단되는 것을 나타낸다.



9. 필터링/정렬 결과 표시

때로는 원본 데이터를 실제로 수정하거나 교체하지 않고 필터링되거나 정렬된 결과를 표시하고 싶을 수 있다. 이 경우 필터링되거나 정렬된 배열을 반환하는 계산된 속성을 만들 수 있다.

// Options API ver
<script>
export default {
  data() {
    return {
      numbers: [1,2,3,4,5]
    };
  },
  computed: {
    evenNumber() {
      return this.numbers.filter(n => n%2 === 0)
    }
  }
};
</script>

<template>
  <li v-for="n in evenNumber" :key="n">{{ n }}</li>
</template>
// Composition API ver
const numbers = ref([1,2,3,4,5])

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n%2 === 0)
})

계산된 속성이 실현 가능하지 않은 상황(ex. 중첩된 v-for 루프 내부)에서는 다음과 같은 방법으로 해결할 수 있다.

// Options API ver
<script>
export default {
  data() {
    return {
      sets: [[1,2,3,4,5], [6,7,8,9,10]]
    };
  },
  methods: {
    even(numbers) {
      return numbers.filter(number => number % 2 === 0)
    }
  }
};
</script>

<template>
  <ul v-for="numbers in sets" :key="numbers">
    <li v-for="n in even(numbers)" :key="n">{{ n }}</li>
  </ul>
</template>

계산된 속성에서 reverse()와 sort() 사용에 주의할 것. 이 두 가지 방법은 원본 배열을 수정하므로 계산된 속성의 getter 함수에서 피해야 한다. 다음 메서드를 호출하기 전에 원본 배열의 복사본을 만든다.

// 원본 배열 numbers을 수정
- return numbers.reverse()
// 원본 배열의 복사본을 만들어서 반환()
+ return [...numbers].reverse()


note! JavaScript에서 사용되는 전개 연산자

전개 구문을 사용하면 배열이나 문자열과 같이 반복 가능한 문자를 0개 이상의 인수(함수로 호출한 경우) 또는 요소(배열 리터럴의 경우)로 확장하여, 0개 이상의 키-값의 쌍으로 객체를 확장시킬 수 있다.

function sum(x,y,z) {
  return x+y+z;
}

const numbers = [1,2,3];

// 함수 호출 구문
console.log(sum(...numbers));
// 배열 리터럴과 문자열
console.log([...numbers, "4", "five", 6]);
// 객체 리터럴
let objClone = { ...numbers };




참고

Vue Docs

전개 구문