changed 3 years ago
Linked with GitHub

Vue2 Reactivity : Vue2의 반응형 동작 원리 살펴보기

vue.js 2의 반응형

모델은 그저 plain js object이다. 모델에 변경이 생기면, 뷰가 업데이트된다.
이 과정이 단순하고, 직관적이지만, 이것이 어떻게 동작하는지를 알아야 한다.

어떻게 변경을 추적하는가

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

순수 자바스크립트 객체가 vue instance의 data option으로 넘겨질 때,
뷰는 그 객체의 모든 프로퍼티를 Object.defineProperty를 사용하여 getter/setter로 바꾼다
. 이는 ES5에만 적용되는 기능이고, 이것이 뷰가 IE8이하는 지원되지 않는 이유가 된다.

getter와 setter는 유저에게는 보이지 않지만, 내부적으로 getter와 setter는 뷰가 의존관계를 추적하여 프로퍼티에 접근하거나 수정될 때, 변화를 알린다.

브라우저 콘솔은 이 getter와 setter를 다른 포맷으로 보여주므로 vue-devtools를 설치하여 관찰하기에 더 좋은 인터페이스로 보는 것이 좋다.

모든 컴포넌트 인스턴스는 그에 대응하는 watcher 인스턴스가 있다. 이 인스턴스는 컴포넌트가 종속적으로 렌더링되는동안 touch되는 모든 프로퍼티를 기록한다. 나중에 이 의존적인 것의 setter가 호출될 경우, 그것을 watcher가 알게 되며, 결과적으로 컴포넌트가 리렌더링이 된다.

  • 💡정리

    • 컴포넌트에는 그에 대응하는 watcher가 있다.
    • 종속적으로 렌더링되는 동안 watcher는 어떠한 프로퍼티든 touch된 것을 기록한다.
    • 나중에, 이 touch된 프로퍼티에 setter가 호출되면, watcher가 컴포넌트를 리렌더링하게 한다.

변화 감지하기

  • 객체

    뷰는 프로퍼티가 추가되거나 삭제되는 것을 감지할 수 없다. 인스턴스가 초기화되는 동안에 프로퍼티를 getter/setter로 바꾸는 작업을 하기 때문에, 프로퍼티는 반드시 data 객체에 존재해야 한다. 그래야 뷰가 data 객체를 변경시키고, reactive하게 만들 수 있다.

    예를 들어, 아래의 a 프로퍼티는 인스턴스가 초기화되는 동안에 getter/setter로 변경되었으므로, reactive하다. 그러나, 초기화가 끝난 후 추가된 b는 reactive하지 않다.

    
    ​​​​var vm = new Vue({
    ​​​​  data: {
    ​​​​    a: 1
    ​​​​  }
    ​​​​})
    ​​​​// `vm.a` is now reactive
    
    ​​​​vm.b = 2
    ​​​​// `vm.b` is NOT reactive
    

    즉, 뷰는 이미 생성된 인스턴스에 새로운 root-level 프로퍼티를 동적으로 추가하는 것을 허용하지 않는다. 그러나, 중첩된 객체에 Vue.set(object, propertyName, value 메소드를 사용하여 반응적인 프로퍼티를 추가할 수 있다.
    vm.$set 인스턴스 메서드를 사용할 수도 있다. 이는 전역 Vue.set의 alias이다.

    ​​​​Vue.set(vm.someObject, 'b', 2)
    ​​​​this.$set(this.someObject, 'b', 2)
    

    만약, 여러개의 프로퍼티를 이미 존재하는 객체에 추가하고 싶다면, 새로운 객체를 만들어서, 원래의 객체와 새로운 객체를 합쳐야 반응적이게 된다.

    ​​​​// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
    ​​​​this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
    
  • 배열

    뷰는 배열에 다음과 같은 일을 하면 변화를 감지할 수 없다.

    • 배열에 인덱스로 접근해서요소를 직접 변경시키는 것 - e.g. vm.items[index] = newValue;
    • 배열의 길이를 변경시키는 것 - e.g. vm.items.length = newLen;

    배열의 요소를 변경시키고 싶다면, Vue.set이나 Array.prototype.splice로 변경시켜야 한다.

    ​​​​// Vue.set
    ​​​​Vue.set(vm.items, indexOfItem, newValue)
    ​​​​// Array.prototype.splice
    ​​​​vm.items.splice(indexOfItem, 1, newValue)
    

    또 다른 방법으로는 vm.$set 인스턴스 메소드를 사용하는 것이다.

    ​​​​vm.$set(vm.items, indexOfItem, newValue)
    

    배열의 길이를 변경시키려면, splice를 사용해야 한다.

    ​​​​vm.items.splice(newLength)
    

반응적인 프로퍼티 선언하기

뷰는 Root-level 반응적인 프로퍼티를 동적으로 추가할 수 없으므로, 뷰 인스턴스가 초기화되는 시점에 모든 데이타 프로퍼티를 선언해야 한다.

var vm = new Vue({
  data: {
    // declare message with an empty value
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

이러한 제약사항이 있는 이유에는 기술적인 이유가 있다.
이러한 제약이

  • 의존성 추적 시스템에서 엣지 케이스를 제거하고,
  • 뷰인스턴스가 타입 체킹 시스템과 함께 더 나이스하게 동작하게 하기 때문이다.

코드의 유지보수면에서도 data객체에 모든 상태를 적는 것이 좋다. 해당 컴포넌트에서 사용하는 모든 상태를 데이타 객체에 넣으면, 나중에 다시 그 코드를 봤을 때도 더 쉽게 이해할 수 잇다.

Async Update Queue

뷰는 DOM 업데이트를 비동기적으로 한다. 데이터의 변화가 감지되면, queue를 열어서, 같은 이벤트 루프에서 일어난 데이터 변화들을 큐에 추가한다. 같은 watcher가 여러번 호출되면, 큐에는 한 번만 푸쉬된다. 오직 하나의 변화만 넣음으로써 불필요한 계산과 DOM 조작을 피할 수 있다.

그 다음 이벤트 루프가 tick되면, 뷰는 queue를 싹 비우고, 실제 작업을 수행한다. 내부적으로 뷰는 비동기 큐를 위해 Promise.then, MutationObserver, setImmediate를 수행하고, setTimeout(fn, 0)으로 돌아간다.

예를 들어, vm.someData = 'newVal'처럼 상태가 변경되면, 컴포넌트는 즉시 리렌더링하지 않는다. 뷰는 다음 tick에 큐를 비우고, 업데이트한다.

이러한 뷰의 동작 방식은 DOM 상태가 업데이트된 후에 의존적인 일을 하고자 하면, 까다로워진다. 뷰는 data-driven 방식을 지향하고, DOM에 직접 접근하는 것을 지양하지만, 직접 접근하는 것이 필요해질 것이다.

data가 변경된 후 뷰가 DOM에 업데이트한 것을 끝낸 것을 보장하기 위해서는, data가 변경된 직후에 바로 Vue.nextTick(callback)을 사용해야 한다. 콜백은 DOM이 업데이트된 이후에 호출된다. vm.$nextTick() 인스턴스 메서드를 사용할 수 있다. 이 인스턴스 메서드의 콜백의 this는 자동으로 현재 뷰 인스턴스에 바인드되어있다.

  • Vue.nextTick(callback)

    vm.$el.textContent는 앞에 공백이 있기 때문에 trim을 해줘야 'new message'와 일치하는지를 제대로 비교할 수 있다.

    ​​​​var vm = new Vue({
    ​​​​  el: '#example',
    ​​​​  data: {
    ​​​​    message: '123'
    ​​​​  },
    ​​​​})
    ​​​​vm.message = 'new message' // change data
    ​​​​console.log(vm.$el.textContent.trim()); // 123
    ​​​​console.log(vm.$el.textContent.trim() === 'new message')// false
    ​​​​Vue.nextTick(function () {
    ​​​​  console.log('next tick');
    ​​​​  console.log(vm.$el.textContent.trim()); // new message
    ​​​​  console.log(vm.$el.textContent.trim() === 'new message')// true
    ​​​​})
    
  • this.$nextTick

    ​​​​Vue.component('example', {
    ​​​​  template: '<span>{{ message }}</span>',
    ​​​​  data: function () {
    ​​​​    return {
    ​​​​      message: 'not updated'
    ​​​​    }
    ​​​​  },
    ​​​​  methods: {
    ​​​​    updateMessage: function () {
    ​​​​      this.message = 'updated'
    ​​​​      console.log(this.$el.textContent) // => 'not updated'
    ​​​​      this.$nextTick(function () {
    ​​​​        console.log(this.$el.textContent) // => 'updated'
    ​​​​      })
    ​​​​    }
    ​​​​  }
    ​​​​})
    
  • async/await
    nextTick은 프라미스를 반환하므로, async/await으로 DOM 업데이트가 끝나기를 기다릴 수 있다.

    ​​​​methods: {
    ​​​​  updateMessage: async function () {
    ​​​​    this.message = 'updated'
    ​​​​    console.log(this.$el.textContent) // => 'not updated'
    ​​​​    await this.$nextTick()
    ​​​​    console.log(this.$el.textContent) // => 'updated'
    ​​​​  }
    ​​​​}
    

참고

Select a repo