Vue 조건부 렌더링과 리스트 렌더링

Vue 렌더링 가이드

조건부 렌더링

v-if

v-if 디렉티브는 조건에 따라 블록을 렌더링하기 위해 사용되는데, 블록은 v-if 디렉티브의 표현식이 true 값을 반환할 때만 렌더링되고, v-else와 함께 “else 블록”을 추가할 수도 있습니다.

v-if 렌더링하기
<h1 v-if="awesome">Vue is awesome!</h1>
v-if에 else 블록 렌더링 추가하기
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no ????</h1>
<template>에 v-if 조건부 그룹 만들기

v-if는 디렉티브이기 때문에 하나의 엘리먼트에 추가해야 하는데, 하나 이상의 엘리먼트를 트랜지션해야 하는 경우에는 다음과 같이 보이지 않는 래퍼 역할을 하는 <template> 엘리먼트에 v-if를 사용할 수 있습니다. <template> 엘리먼트는 최종 렌더링 결과에는 포함되지 않습니다.

template에 v-if 렌더링하기
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-else

v-else 디렉티브는 v-if에 대한 “else 블록”을 나타낼 수 있습니다. v-else 엘리먼트는 v-if 엘리먼트 또는 v-else-if 엘리먼트 바로 뒤에 있어야 되는데, 그렇지 않으면 Vue가 v-else를 인식할 수 없기 때문에 주의해야 합니다.

v-else 렌더링하기
<div v-if="Math.random() > 0.5">
  이제 나를 볼 수 있어요
</div>
<div v-else>
  이제는 안보입니다
</div>

v-else-if

v-else-if는 2.1.0+부터 새롭게 추가된 디렉티브로, v-if에 대한 “else if 블록” 역할을 하는 디렉티브입니다. v-else-if는 여러 개를 사용할 수 있는데, v-else와 마찬가지로 v-else-if 엘리먼트는 v-if 또는 v-else-if 엘리먼트의 바로 뒤에 와야 합니다.

v-else-if 렌더링하기
<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

key로 엘리먼트 렌더링 제어하기

Vue는 엘리먼트를 가능한 효율적으로 렌더링하기위해 엘리먼트를 처음부터 렌더링을 하지않고 다시 사용할 수 있는데, 엘리먼트를 재사용하는 것은 Vue를 매우 빠르게 만드는데 도움이 되고 이외에도 몇가지 유용한 이점이 있습니다.

사용자가 여러 로그인 유형을 트랜지션할 수 있도록 허용하는 경우에도 유용한데, 다음 코드와 같이 loginType을 바꾸어도 input에 사용자가 이미 입력한 내용은 지워지지 않고, 두 템플릿이 모두 같은 요소를 사용하기 때문에 <input>은 대체되지 않고 placeholder만 변경되는 동작을 하게 됩니다.

엘리먼트를 재사용하는 일반 코드
<template v-if="loginType === 'username'">
  <label>사용자 이름</label>
  <input placeholder="사용자 이름을 입력하세요">
</template>
<template v-else>
  <label>이메일</label>
  <input placeholder="이메일 주소를 입력하세요">
</template>

그런데 위와 같이 엘리먼트를 재사용하는 것이 항상 바람직한 것은 아니기때문에, 두 엘리먼트를 완전히 별개의 엘리먼트로 인식시키기 위해 유일한 값을 key 속성으로 추가해서 사용할 수도 있습니다.

key를 추가하여 처음부터 렌더링되는 코드
<template v-if="loginType === 'username'">
  <label>사용자 이름</label>
  <input placeholder="사용자 이름을 입력하세요" key="username-input">
</template>
<template v-else>
  <label>이메일</label>
  <input placeholder="이메일 주소를 입력하세요" key="email-input">
</template>

위의 코드에서는 input에 각각 username-input, email-input이라는 key를 추가했는데, 트랜지션이 이루어지는 경우 input에 입력했던 값이 초기화되고 처음부터 렌더링되는 것을 확인할 수 있습니다.

참고로 <label> 엘리먼트는 key 속성이 없기 때문에 여전히 효율적으로 재사용될 수 있습니다.

v-show

v-show 디렉티브는 엘리먼트를 조건부로 표시하기 위한 또 다른 옵션입니다. 사용법은 v-if와 거의 동일한데, v-show가 있는 엘리먼트는 항상 렌더링 되고 DOM에 남아있다는 차이가 있습니다. 즉 v-show는 단순히 엘리먼트에 CSS의 display 속성을 토글하는 디렉티브입니다.

v-show 렌더링하기
<h1 v-show="ok">안녕하세요!</h1>

리스트 렌더링

v-for와 배열 매핑

v-for 디렉티브는 배열을 기반으로 리스트를 렌더링 할 수 있습니다. v-for 디렉티브는 리스트를 렌더링할 때 item in items 형태의 특별한 문법이 필요한데, items는 원본 데이터 배열을 뜻하고, item은 반복되는 배열 엘리먼트의 별칭을 뜻합니다.

v-for 렌더링하기
<ul id="example-1">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>
v-for 렌더링 데이터
var example1 = new Vue({
  el: '#example-1',
  data: {
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

v-for 블록에는 부모 범위 속성에 대한 모든 권한을 가지고 있고, 현재 항목의 인덱스에 대한 두 번째 전달인자 옵션도 제공할 수 있습니다.

v-for에서 index 사용하기
<ul id="example-2">
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>
v-for 렌더링 데이터
var example2 = new Vue({
  el: '#example-2',
  data: {
    parentMessage: 'Parent',
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

v-for의 반복 문법에서는 in 대신, JavaScript의 이터레이터에 대한 자바스크립트 구문과 유사한 of를 구분자로 사용할 수도 있습니다.

v-for에 of 구분자 사용하기
<div v-for="item of items"></div>

v-for와 객체

v-for는 객체의 속성을 반복하거나 키에 대한 두 번째 전달 인자를 제공할 수도 있습니다.

v-for에 객체 사용하기
<ul id="v-for-object" class="demo">
  <li v-for="value in object">
    {{ value }}
  </li>
</ul>
v-for 객체 데이터
new Vue({
  el: '#v-for-object',
  data: {
    object: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
})

v-for에 객체를 사용하는 경우 다음과 같이 키에 대한 두 번째 전달 인자를 제공하거나 인덱스를 제공할 수 있는데, 객체를 반복하는 경우에는 Object.keys()의 키 나열 순서에 따라 결정되기 때문에 배열과 같이 일정한 순서로 렌더링되지는 않습니다.

키에 대한 두 번째 인자 제공하기
<div v-for="(value, name) in object">
  {{ name }}: {{ value }}
</div>
인덱스 제공하기
<div v-for="(value, name, index) in object">
  {{ index }}. {{ name }}: {{ value }}
</div>

Maintaining State

Vue가 v-for에서 렌더링된 엘리먼트 목록을 갱신하는 경우에는 기본적으로 “in-place patch”라는 전략을 사용하는데, 이는 데이터 항목의 순서가 변경된 경우에 항목의 순서와 일치하도록 DOM 요소를 이동하는 대신 각 요소를 적절한 위치에 패치하고 해당 인덱스에서 렌더링할 내용을 반영하는지 확인합니다.

이 기본 모드는 효율적이지만 목록의 출력 결과가 하위 컴포넌트 상태이거나 폼 input과 같은 임시 DOM 상태에 의존하지 않는 경우에 적합합니다.

Vue에서 개별 DOM 노드들을 추적하고 기존 엘리먼트를 재사용, 재정렬하기 위해서는 v-for의 각 항목들에 고유한 key 속성을 제공해야 되는데, key에 대한 이상적인 값은 각 항목을 식별할 수 있는 고유한 ID가 될 수 있고, 이 특별한 속성은 v-bind를 사용하여 동적 값에 바인딩을 해주어야 합니다.

v-for에 고유 ID 바인딩하기
<div v-for="item in items" v-bind:key="item.id">
  <!-- content -->
</div>

고유 ID를 바인딩하는 방식은 반복되는 DOM 내용이 단순한 경우나 의도적인 성능 향상을 위해 기본 동작에 의존하지 않는 경우를 제외하면, 언제나 v-for에 key를 추가하는 것이 좋습니다.

단 객체나 배열처럼, 기본 타입이 아닌 값을 키로 사용해서는 안되고, 문자열이나 숫자를 사용해야 하는데, key는 Vue가 노드를 식별하는 일반적인 메커니즘이기 때문에 v-for와 특별히 연관되지 않는 다른 용도로도 사용될 수 있습니다.

배열 변경 감지

변이 메소드

Vue는 감시중인 배열의 변이 메소드를 래핑하여 뷰 갱신을 트리거하는데, 다음과 같이 래핑된 메소드들은 콘솔을 열고 이전 예제의 items와 같은 배열로 example1.items.push({ message: 'Baz' })와 같이 변이 메소드를 호출하여 재생할 수 있습니다.

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()
배열 대체

변이 메소드는 호출된 원본 배열을 변형하는데, 이와 비교하여 변형을 하지 않는 방법도 있습니다. 바로 filter(), concat(), slice() 메소드인데, 이 메소드들을 사용하면 원본 배열을 변형하지 않고 항상 새 배열을 반환하게 됩니다.

원본 배열을 변형하지 않는 메소드
example1.items = example1.items.filter(function (item) {
  return item.message.match(/Foo/)
})

이렇게 하면 Vue가 기존 DOM을 버리고 전체 목록을 다시 렌더링 한다고 생각할 수 있습니다. 하지만 다행히도, 그렇지는 않습니다. Vue는 DOM 요소 재사용을 극대화하기 위해 몇가지 똑똑한 구현을 하므로 배열을 겹치는 객체가 포함된 다른 배열로 대체하기 때문에 효율적입니다.

배열 변경 시 주의 사항

JavaScript의 제한으로 인해 Vue는 배열에 대해 다음과 같은 변경 사항을 감지할 수 없습니다.

  • vm.items[indexOfItem] = newValue와 같이 인덱스로 배열에 있는 항목을 직접 설정하는 경우
  • vm.items.length = newLength와 같이 배열 길이를 수정하는 경우
배열 변경에 반응하지 않는 경우
var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // reactive하지 않음
vm.items.length = 2 // reactive하지 않음

위의 코드의 첫 번째와 같이 인덱스로 배열에 있는 항목을 직접 설정하는 경우에는 다음과 같은 코드로 반응 시스템을 트리거할 수 있습니다.

Vue.set 사용하기
Vue.set(vm.items, indexOfItem, newValue)
Array.prototype.splice 사용하기
vm.items.splice(indexOfItem, 1, newValue)
인스턴스 메소드 vm.$set 사용하기
// vm.$set은 전역 Vue.set의 별칭
vm.$set(vm.items, indexOfItem, newValue)

두 번째와 같이 배열 길이를 수정하는 경우에는 다음과 같은 코드로 반응 시스템을 트리거할 수 있습니다.

splice 사용하기
vm.items.splice(newLength)
객체 변경 시 주의 사항
Vue 객체에 속성 추가하기
var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 는 반응형

vm.b = 2
// `vm.b` 는 반응형이 아님

모던 JavaScript의 한계로 Vue는 속성 추가 및 삭제를 감지하지 못하는데, Vue는 이미 만들어진 인스턴스에 새로운 루트레벨의 반응형 속성을 동적으로 추가하는 것을 허용하지 않습니다. 하지만 Vue.set(object, propertyName, value) 메소드를 사용하면 중첩된 객체에 반응형 속성을 추가할 수 있습니다.

Vue 객체 데이터
var vm = new Vue({
  data: {
    userProfile: {
      name: 'Anika'
    }
  }
})

Vue는 위와 같은 객체에 다음의 방법으로 새로운 속성을 추가할 수 있습니다.

중첩된 userProfile 객체에 새로운 속성 age 추가하기
Vue.set(vm.userProfile, 'age', 27)
인스턴스 메소드 vm.$set 사용하기
vm.$set(vm.userProfile, 'age', 27)

때로는 Object.assign()이나 _.extend()를 사용해 기존의 객체에 새 속성을 할당할 수 있는데, 이 경우에는 두 객체의 속성을 사용해 새 객체를 만들어 주어야 합니다.

Object.assign()으로 새 속성 할당하기
Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})
새로운 반응형 속성 추가하기
vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

필터링/정렬 결과 표시하기

원본 데이터를 실제로 변경하거나 재설정하지 않고 배열의 필터링 된 버전이나 정렬된 버전을 표시해야 하는 경우가 있는데, 이런 경우 필터링 된 배열이나 정렬된 배열을 반환하는 계산된 속성을 만들 수 있습니다.

v-for 렌더링하기
<li v-for="n in evenNumbers">{{ n }}</li>
계산된 속성으로 배열 반환하기
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

중첩 된 v-for 루프의 내부와 같이 계산된 속성을 실행할 수 없는 상황에서는 다음과 같이 methods 속성을 사용할 수 있습니다.

v-for 렌더링하기
<li v-for="n in even(numbers)">{{ n }}</li>
methods 속성으로 배열 반환하기
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
  even: function (numbers) {
    return numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

v-for Range

v-for는 배열 대신 숫자를 사용할 수 있는데, 이 경우 템플릿을 여러번 반복합니다.

특정 Range 지정하기
<div>
  <span v-for="n in 10">{{ n }} </span>
</div>

v-for 템플릿

v-for는 v-if 템플릿과 마찬가지로, <template>태그를 사용해 여러 엘리먼트의 블럭을 렌더링 할 수 있습니다.

v-for 템플릿 렌더링하기
<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

v-for와 v-if

Vue는 v-if와 v-for를 동시에 사용하는 것을 추천하지 않습니다. 만약 동일한 노드에 두 가지의 디렉티브가 모두 있다면, v-forv-if보다 높은 우선순위를 갖게 되는데, 이 경우 v-if는 루프가 반복될 때마다 실행이 되고, 이는 일부 항목만 렌더링 하려는 경우에 유용하게 사용될 수 있습니다.

v-for와 v-if 동시에 사용하기
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>

위 코드의 경우 완료되지 않은 할일만 렌더링하게 되는데, 위의 방법 대신 실행을 조건부로 하는 것이 목적이라면 래퍼 엘리먼트<template>을 사용하는 것이 좋습니다.

래퍼 엘리먼트 사용하기
<ul v-if="todos.length">
  <li v-for="todo in todos">
    {{ todo }}
  </li>
</ul>
<p v-else>No todos left!</p>

v-for와 컴포넌트

v-for는 사용자 정의 컴포넌트에 직접 사용할 수 있는데, 2.2.0+ 버전에서 v-forkey가 필수 속성이기 때문에 반드시 함께 사용해주어야 합니다.

컴포넌트에 v-for 사용하기
<my-component v-for="item in items" :key="item.id"></my-component>

그런데 컴포넌트는 자체 범위가 분리되어있어 컴포넌트에 데이터를 자동으로 전달하지는 않습니다. 만약 반복할 데이터를 컴포넌트로 전달하고 싶다면 반드시 props를 함께 사용해 주어야 합니다.

컴포넌트에 props 사용하기
<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>

컴포넌트에 item을 자동으로 주입하지 않는 이유는 컴포넌트가 v-for의 작동 방식과 밀접하게 결합되기 때문인데, 데이터의 출처를 명확히 하면 다른 상황에서도 컴포넌트를 재사용할 수 있기 때문입니다.

다음 할 일 목록 예제 코드에서 is="todo-item" 속성은 <li> 엘리먼트는 <ul> 안에서만 유효합니다. 즉 <todo-item>과 같은 사용자 정의 컴포넌트와 동일한 작업을 한다고 볼 수 있지만, 이런 방식은 잠재적인 브라우저의 구문 분석 오류를 해결할 수 있습니다.

할 일 목록 예제 컴포넌트
<div id="todo-list-example">
  <form v-on:submit.prevent="addNewTodo">
    <label for="new-todo">Add a todo</label>
    <input
      v-model="newTodoText"
      id="new-todo"
      placeholder="E.g. Feed the cat"
    >
    <button>Add</button>
  </form>
  <ul>
    <li
      is="todo-item"
      v-for="(todo, index) in todos"
      v-bind:key="todo.id"
      v-bind:title="todo.title"
      v-on:remove="todos.splice(index, 1)"
    ></li>
  </ul>
</div>
할 일 목록 예제 스크립트
Vue.component('todo-item', {
  template: '\
    <li>\
      {{ title }}\
      <button v-on:click="$emit(\'remove\')">Remove</button>\
    </li>\
  ',
  props: ['title']
})

new Vue({
  el: '#todo-list-example',
  data: {
    newTodoText: '',
    todos: [
      {
        id: 1,
        title: 'Do the dishes',
      },
      {
        id: 2,
        title: 'Take out the trash',
      },
      {
        id: 3,
        title: 'Mow the lawn'
      }
    ],
    nextTodoId: 4
  },
  methods: {
    addNewTodo: function () {
      this.todos.push({
        id: this.nextTodoId++,
        title: this.newTodoText
      })
      this.newTodoText = ''
    }
  }
})

답글 남기기