owned this note
owned this note
Published
Linked with GitHub
---
tags: vue
---
# Vue2 Reactivity : Vue2의 반응형 동작 원리 살펴보기
## vue.js 2의 반응형
모델은 그저 plain js object이다. 모델에 변경이 생기면, 뷰가 업데이트된다.
이 과정이 단순하고, 직관적이지만, 이것이 어떻게 동작하는지를 알아야 한다.
### 어떻게 변경을 추적하는가

<img width='70%' src='https://i.imgur.com/cthH0g3.png' />
**순수 자바스크립트 객체가 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하지 않다.
```javascript
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이다.
```javascript
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject, 'b', 2)
```
만약, 여러개의 프로퍼티를 이미 존재하는 객체에 추가하고 싶다면, 새로운 객체를 만들어서, 원래의 객체와 새로운 객체를 합쳐야 반응적이게 된다.
```javascript
// 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로 변경시켜야 한다.
```javascript
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
```
또 다른 방법으로는 vm.$set 인스턴스 메소드를 사용하는 것이다.
```javascript
vm.$set(vm.items, indexOfItem, newValue)
```
배열의 길이를 변경시키려면, splice를 사용해야 한다.
```javascript
vm.items.splice(newLength)
```
### 반응적인 프로퍼티 선언하기
뷰는 Root-level 반응적인 프로퍼티를 동적으로 추가할 수 없으므로, **뷰 인스턴스가 초기화되는 시점에 모든 데이타 프로퍼티를 선언**해야 한다.
```javascript
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'와 일치하는지를 제대로 비교할 수 있다.
```javascript
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
```javascript
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 업데이트가 끝나기를 기다릴 수 있다.
```javascript
methods: {
updateMessage: async function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'updated'
}
}
```
## 참고
- [Reactivity in Depth - vue2](https://vuejs.org/v2/guide/reactivity.html)