본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로웹 개발 › 프론트엔드 › Vue

v-for 반복문

목차

v-for 반복문

메뉴 목록의 맨 앞에 새 항목을 추가했는데 입력 중이던 값이 다른 행으로 옮겨 보인다면 v-for 문법보다 행의 정체성을 어떻게 지정했는지 먼저 확인해야 한다. 반복 렌더링은 배열을 HTML로 바꾸는 일과 기존 DOM을 어떤 데이터에 대응시킬지를 결정하는 일을 함께 수행한다.

여기서는 기존 글과 같은 Vue 2 Options API를 사용한다. Vue 2는 지원이 종료된 버전이며, 예제는 기존 코드 학습용이다. Vue 2 안내

객체 배열을 목록으로 렌더링하기

<div id="app">
  <label><input type="checkbox" v-model="onlyAvailable">판매 중만 보기</label>
  <button type="button" @click="reverse">순서 뒤집기</button>
  <ul>
    <li v-for="(menu, index) in visibleMenus" :key="menu.id">
      {{ index + 1 }}. {{ menu.name }}: {{ menu.price }}원
      <button type="button" @click="remove(menu.id)">삭제</button>
    </li>
  </ul>
  <p v-if="visibleMenus.length === 0">표시할 메뉴가 없습니다.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
  el: '#app',
  data: {
    onlyAvailable: false,
    menus: [
      { id: 'm1', name: '짬뽕', price: 7000, available: true },
      { id: 'm2', name: '짜장면', price: 6000, available: false },
      { id: 'm3', name: '탕수육', price: 11000, available: true }
    ]
  },
  computed: {
    visibleMenus() {
      return this.onlyAvailable
        ? this.menus.filter(menu => menu.available)
        : this.menus;
    }
  },
  methods: {
    reverse() {
      this.menus = [...this.menus].reverse();
    },
    remove(id) {
      this.menus = this.menus.filter(menu => menu.id !== id);
    }
  }
});
</script>

처음에는 세 메뉴가 나타난다. 판매 중만 보기를 선택하면 m2가 제외되고 표시 번호는 다시 1, 2가 된다. 표시 번호는 현재 목록의 위치이고, id는 필터와 정렬이 바뀌어도 메뉴 자체를 식별한다. 삭제 함수에 index 대신 id를 넘기는 이유도 같다. 필터링한 목록의 1번 위치가 원본 배열의 1번 위치와 같다는 보장은 없다.

v-for="(menu, index) in visibleMenus"의 menu는 현재 항목, index는 0부터 시작하는 위치다. 정수 범위를 쓰는 v-for="n in 9"의 n은 1부터 9까지 나온다. 둘의 시작값을 혼동하면 구구단이나 페이지 번호에서 한 칸씩 어긋난다.

key가 없거나 index인 경우

Vue는 이전 렌더링 결과와 새 결과를 비교해 DOM을 갱신한다. 기본적인 제자리 갱신은 단순 텍스트 목록에는 충분할 수 있지만, 각 행에 입력 요소나 상태를 가진 자식 컴포넌트가 있으면 데이터와 DOM 상태의 대응이 중요하다. Vue 2 목록 렌더링

변경 전 데이터: [A(id=10), B(id=20)]
맨 앞 삽입 후:  [X(id=30), A(id=10), B(id=20)]

index를 key로 사용: key=0은 A에서 X로 의미가 바뀜
id를 key로 사용:    key=10은 전후 모두 A를 가리킴

key는 같은 형제 목록 안에서 유일하고 안정적인 원시 값이어야 한다. 렌더링할 때마다 난수를 만들면 매번 새 행처럼 취급될 수 있고, 객체 자체를 key로 넘기는 것도 피한다. 서버 ID가 없다면 항목 생성 시 ID를 부여해 상태에 보관한다. 표시 이름은 중복·변경될 수 있으므로 항상 안전한 key는 아니다.

필터와 정렬을 렌더링에서 분리하기

computed에 필터링을 두면 템플릿에서는 어떤 배열을 그리는지만 읽을 수 있다. v-if와 v-for를 같은 행에 붙이는 방식보다 조건의 범위도 분명해진다. 정렬할 때는 원본 배열을 직접 바꾸는 sort()의 특성을 주의한다.

computed: {
  byPrice() {
    return [...this.menus].sort((a, b) => a.price - b.price);
  }
}

복사 후 정렬하면 computed를 읽었다는 이유로 원본 순서가 바뀌지 않는다. 위에서 reverse()도 복사한 배열에 적용했다. 객체의 일부 값을 변경할 때는 map과 객체 복사를 조합해 바뀐 항목을 명시한다.

this.menus = this.menus.map(menu =>
  menu.id === targetId ? { ...menu, price: nextPrice } : menu
);

Vue 2에서는 배열 인덱스 직접 대입과 길이 직접 변경에 반응성 제약이 있다. 새 배열을 할당하는 방식은 이 제약을 피하면서 상태 변경을 한곳에 모아 준다. Vue 3에서의 감지 방식과 동일하다고 일반화하지 않는다.

중첩 반복으로 표 만들기

표의 행과 열에는 각각의 식별자가 필요하다. 아래는 템플릿 부분이다. columns와 rows를 Vue의 데이터에 선언해 사용한다.

<table>
  <thead>
    <tr><th v-for="column in columns" :key="column.key" scope="col">{{ column.label }}</th></tr>
  </thead>
  <tbody>
    <tr v-for="row in rows" :key="row.id">
      <td v-for="column in columns" :key="column.key">{{ row[column.key] }}</td>
    </tr>
  </tbody>
</table>

thead 안에는 tr이 필요하다. 행이 단순 배열이면 열 추가 시 인덱스 의미를 함께 바꿔야 하지만 {name, price}처럼 이름을 가진 객체와 열 정의를 분리하면 표시 순서를 바꾸기 쉽다.

항목이 수만 개라면 안정적인 key만으로 렌더링 비용이 해결되지는 않는다. 모든 행의 DOM과 이벤트를 유지해야 하기 때문이다. 화면에 필요한 데이터만 가져오는 페이지네이션이나 가상 스크롤을 검토하고, 먼저 실제 행 수와 업데이트 빈도를 측정한다. v-for의 책임은 목록 대응과 렌더링이며 데이터 양 자체를 줄여 주지는 않는다.

같은 카테고리의 글