v-for는 배열이나 객체를 순회하며 엘리먼트를 반복 렌더링하는 디렉티브입니다. key 속성을 올바르게 사용하는 것이 성능의 핵심입니다.

면접에서 "v-for에 key를 왜 써야 하나요?"라는 질문은 정말 자주 나옵니다. "Vue가 경고하니까요"가 아니라, 가상 DOM의 diff 알고리즘 과 연결해서 설명할 수 있어야 합니다.


기본 사용법

VUE
<script setup lang="ts">
import { ref } from 'vue'

const fruits = ref(['사과', '바나나', '오렌지', '포도'])

const users = ref([
  { id: 1, name: '김개발', role: 'frontend' },
  { id: 2, name: '이서버', role: 'backend' },
  { id: 3, name: '박디비', role: 'dba' }
])

// 객체 순회
const profile = ref({
  name: '심정훈',
  age: 25,
  job: '개발자'
})
</script>

<template>
  <!-- 배열 순회 — (요소, 인덱스) -->
  <ul>
    <li v-for="(fruit, index) in fruits" :key="fruit">
      {{ index }}. {{ fruit }}
    </li>
  </ul>

  <!-- 객체 배열 순회 — 고유 id를 key로 사용 -->
  <div v-for="user in users" :key="user.id">
    <span>{{ user.name }} ({{ user.role }})</span>
  </div>

  <!-- 객체 순회 — (값, 키, 인덱스) -->
  <div v-for="(value, key, index) in profile" :key="key">
    {{ index }}. {{ key }}: {{ value }}
  </div>

  <!-- 숫자 범위 — 1부터 시작 -->
  <span v-for="n in 5" :key="n">{{ n }} </span>
  <!-- 출력: 1 2 3 4 5 -->
</template>

key의 중요성

key는 Vue의 가상 DOM diff 알고리즘이 노드를 식별하는 데 사용됩니다.

VUE
<script setup lang="ts">
import { ref } from 'vue'

interface Todo {
  id: number
  text: string
  done: boolean
}

const todos = ref<Todo[]>([
  { id: 1, text: '공부하기', done: false },
  { id: 2, text: '운동하기', done: false },
  { id: 3, text: '밥먹기', done: true }
])

const addTodo = () => {
  // 배열 앞에 추가
  todos.value.unshift({
    id: Date.now(),
    text: '새 할일',
    done: false
  })
}

const removeTodo = (id: number) => {
  todos.value = todos.value.filter(t => t.id !== id)
}
</script>

<template>
  <button @click="addTodo">추가</button>

  <!-- 나쁜 예: index를 key로 사용 -->
  <!-- 배열 앞에 요소가 추가되면 모든 인덱스가 밀림 → 불필요한 리렌더링 -->
  <div v-for="(todo, index) in todos" :key="index">
    <input type="checkbox" v-model="todo.done" />
    {{ todo.text }}
  </div>

  <!-- 좋은 예: 고유 id를 key로 사용 -->
  <div v-for="todo in todos" :key="todo.id">
    <input type="checkbox" v-model="todo.done" />
    {{ todo.text }}
    <button @click="removeTodo(todo.id)">삭제</button>
  </div>
</template>

key에 index를 쓰면 안 되는 이유

  1. 배열 중간에 삽입/삭제 시 인덱스가 전부 밀림
  2. Vue가 같은 key = 같은 요소 로 판단하여 기존 DOM을 재사용
  3. 상태가 있는 컴포넌트(체크박스, 입력값)에서 잘못된 상태가 매핑됨

배열 변경 감지

Vue는 반응형 배열의 변이 메서드를 감지합니다.

TYPESCRIPT
import { ref } from 'vue'

const items = ref([1, 2, 3])

// 변이 메서드 — Vue가 자동으로 업데이트 트리거
items.value.push(4)       // 끝에 추가
items.value.pop()         // 끝에서 제거
items.value.shift()       // 앞에서 제거
items.value.unshift(0)    // 앞에 추가
items.value.splice(1, 1)  // 인덱스 1에서 1개 제거
items.value.sort()        // 정렬
items.value.reverse()     // 역순

// 배열 교체 — 새 배열을 할당해도 Vue가 효율적으로 DOM 업데이트
items.value = items.value.filter(n => n > 2)
items.value = [...items.value, 5, 6]

v-for와 v-if 함께 사용하기

Vue 3에서는 v-if가 v-for보다 우선순위가 높습니다. (Vue 2에서는 반대)

VUE
<script setup lang="ts">
import { ref, computed } from 'vue'

interface Todo {
  id: number
  text: string
  done: boolean
}

const todos = ref<Todo[]>([
  { id: 1, text: '공부하기', done: false },
  { id: 2, text: '운동하기', done: true },
  { id: 3, text: '밥먹기', done: false }
])

// 권장: computed로 필터링
const activeTodos = computed(() =>
  todos.value.filter(todo => !todo.done)
)
</script>

<template>
  <!-- 나쁜 예: v-for와 v-if를 같은 엘리먼트에 -->
  <!-- v-if가 먼저 평가되므로 todo 변수에 접근 불가 -->
  <!-- <li v-for="todo in todos" v-if="!todo.done" :key="todo.id"> -->

  <!-- 좋은 예 1: computed로 필터링 -->
  <li v-for="todo in activeTodos" :key="todo.id">
    {{ todo.text }}
  </li>

  <!-- 좋은 예 2: template으로 감싸기 -->
  <template v-for="todo in todos" :key="todo.id">
    <li v-if="!todo.done">
      {{ todo.text }}
    </li>
  </template>
</template>

template에서 v-for

여러 엘리먼트를 래퍼 없이 반복하려면 <template>에 v-for를 사용합니다.

VUE
<template>
  <table>
    <tbody>
      <!-- template은 실제 DOM에 렌더링되지 않음 -->
      <template v-for="user in users" :key="user.id">
        <tr>
          <td>{{ user.name }}</td>
          <td>{{ user.role }}</td>
        </tr>
        <tr>
          <td colspan="2">{{ user.description }}</td>
        </tr>
      </template>
    </tbody>
  </table>
</template>

컴포넌트에서 v-for

VUE
<script setup lang="ts">
import { ref } from 'vue'
import UserCard from './UserCard.vue'

const users = ref([
  { id: 1, name: '김개발' },
  { id: 2, name: '이서버' }
])

const removeUser = (id: number) => {
  users.value = users.value.filter(u => u.id !== id)
}
</script>

<template>
  <!-- 컴포넌트에 v-for 사용 시 반드시 key와 props 명시 -->
  <UserCard
    v-for="user in users"
    :key="user.id"
    :name="user.name"
    @remove="removeUser(user.id)"
  />
</template>

성능 최적화 팁

VUE
<script setup lang="ts">
import { ref, computed, shallowRef } from 'vue'

// 1. 대량 데이터는 shallowRef 고려
// 깊은 반응형이 필요 없을 때 성능 향상
const bigList = shallowRef<Array<{ id: number; value: string }>>([])

// 2. computed로 필터링/정렬을 캐싱
const sortedList = computed(() =>
  [...bigList.value].sort((a, b) => a.value.localeCompare(b.value))
)

// 3. 가상 스크롤 — 수천 개 아이템은 전부 렌더링하지 말 것
// vue-virtual-scroller 같은 라이브러리 활용
</script>

<template>
  <div v-for="item in sortedList" :key="item.id">
    {{ item.value }}
  </div>
</template>

면접 팁

  • key에 index를 쓰면 안 되는 구체적인 시나리오 를 설명할 수 있어야 합니다 (체크박스 상태 꼬임 등)
  • v-for와 v-if의 우선순위가 Vue 2와 3에서 반대 라는 것은 마이그레이션 관련 질문에서 나올 수 있습니다
  • 대량 리스트 렌더링 시 가상 스크롤(Virtual Scroll) 을 언급하면 실무 경험이 있다는 인상을 줍니다

요약

v-for는 배열, 객체, 숫자 범위를 순회하며 반복 렌더링합니다. key는 반드시 고유한 식별자를 사용하고, index는 상태가 있는 리스트에서 문제를 일으킵니다. v-for와 v-if는 같은 엘리먼트에 함께 쓰지 말고, computed로 필터링하거나 template으로 분리하세요.

댓글 로딩 중...