[Vue.js] Vue 3 Reactivity (#1)

하수도키

·

2020. 7. 22. 18:25

728x90
반응형
SMALL

[Vue.js] Vue 3 Reactivity (#1)

개요

  • 이 문서는 www.vuemastery.comVue 3 Reactivity 를 보고 정리했습니다.
  • Vue3가 드디어 릴리즈가 되었다. 아직 베타인것 같지만...
  • Vue3 Reactivity System(반응형)에 대해 알아보자.
  • 이걸 알면 좋은 점은??

    • Vue 내부 디자인 패턴 이해
    • Vue 디버깅 스킬 향상
    • Vue3 modularzied 반응형 라이브러리 사용
    • Vue3 소스 코드에 기여 가능
  • 어마어마한 혜택들이 있다.

반응형(Reactivity) 이해하기

  • Vue 반응형은 정말 매직과 같다. 아래 코드로 어떻게 작동하는지 살펴보자.
<template>
  <div id="app">
    <div>Price: ${{ product.price }}</div>
    <div>Total: ${{ product.price * product.quantity }}</div>
    <div>Taxes: ${{ totalPriceWithText }}</div>
  </div>
</template>

<script>
export default {
  name: "App",
  data: () => ({
    product: {
      price: 5.00,
      quantity: 2,
    }
  }),
  computed: {
    totalPriceWithText() {
      return this.product.price * this.product.quantity * 1.03
    }
  }
};
</script>
  • Vue 반응형은 price 가 변하면 3가지 일을 한다.
    • price 값 웹페이지에 갱신
    • 표현식 price * quantity 갱신
    • totalPriceWithTax 함수 다시 호출 후 갱신
  • Vue 반응형은 price 가 변경될때 무엇이 갱신해야 하는지 어떻게 알까?
  • 이건 일반적인 JavaScript 작동하는 방법이 아니다.
let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity // 10
product.price = 20
console.log(`total is ${total}`) // 10
  • total의 값을 40을 기대했지만 10이 나온다.
  • JavaScript 는 절차적이고 반응형이 아니므로 price 의 변경을 바로 반영하여 total 의 값을 변경하지 않는다.

반응형 간단한 코드로 이해해보기

  • 위에 설명한 코드를 JavaScript 에서 반응형으로 작동하기 위해서는 아래와 같은 순서로 작업이 필요하다.
    • total = product.price * product.quantity 이 부분 코드를 저장하고
    • price 가 변경되면 다시 저장한 코드를 호출한다.
let product = { price: 5, quantity: 2 };
let total = 0;

let effect = () => { total = product.price * product.quantity }

track() // effect 함수를 저장하는 역할 나중에 다시 설명
effect()
  • track() 을 정의하기 위해서는 effect 들을 저장할 장소가 필요하다.
  • dep 이라는 종속성있는 Set 을 만든다.
    • Set 객체는 자료형에 관계 없이 원시 값과 객체 참조 모두 유일한 값을 저장할 수 있습니다.
  • 종속성이라고 부르는 이유는 옵저버 패턴을 참고했다. 옵저버 패턴에서 종속성은 subscribers(여기선 effect)를 가지고 있다.
  • 객체 상태가 변경될때 subscribers에게 알려준다.
  • 위 내용을 정리한 코드이다.
let dep = new Set()
function track() {
    dep.add(effect)
}
  • 이제 실행할 함수를 작성해보자.
function trigger() { 
  dep.forEach(effect => effect()) 
}

product.price = 20
console.log(total) // => 10
trigger()
console.log(total) // => 40
  • 위에 내용들을 정리한 코드
let product = { price: 5, quantity: 2 }
let total = 0
let dep = new Set()

function track() {
  dep.add(effect)
}

function trigger() {
  dep.forEach(effect => effect())
}

let effect = () => {
  total = product.price * product.quantity
}

track()
effect()

product.price = 20
console.log(total) // => 10

trigger()
console.log(total) // => 40

다수의 속성들은 어떻게 반응형으로 만들까?

  • 문제가 발생했다. 바로 다수의 속성들일 경우 어떻게 처리할까?
  • 현재까지 객체의 price 만 생각했는데 quantity 로 반응형으로 처리해야 될 경우는?
  • depsMap 를 만들어 처리한다.
    • depsMapMap 자료구조를 가진다.
    • Map 객체는 키-값 쌍을 저장하며 각 쌍의 삽입 순서도 기억하는 콜렉션입니다. 아무 값(객체와 원시 값)이라도 키와 값으로 사용할 수 있습니다.
  • depsMap 에 새로운 효과(effect)를 속성 이름(key)에 추가하는 법을 살펴보자.
const depsMap = new Map()
function track(key) {
  // Make sure this effect is being tracked.
  let dep = depsMap.get(key) // key로 depsMap에 저장되어 있는 값(effect가 있는 저장소)을 가져오자
  if (!dep) {
    // dep이 없다면 새로 저장소를 만들자.
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }
  dep.add(effect) // dep에 effect를 추가
}
  }
function trigger(key) {
  let dep = depsMap.get(key) // key를 이용해 dep을 가져오자 다시 한번 말하지만 effect가 있는 저장소
  if (dep) { // dep이 있을때만 실행
    dep.forEach(effect => {
      // run them all
      effect()
    })
  }
}

let product = { price: 5, quantity: 2 }
let total = 0

let effect = () => {
  total = product.price * product.quantity
}

track('quantity')
effect()
console.log(total) // --> 10

product.quantity = 3
trigger('quantity')
console.log(total) // --> 40

다수의 객체들일때는?

  • 현재까지 하나의 객체만 살펴보았지만 이번 경우는 다수의 객체들일때 문제점을 파악하고 해결해보자.
  • WeakMap 을 사용한다.
    • WeakMap 객체는 키가 약하게 참조되는 키/값 쌍의 컬렉션입니다. 키는 객체여야만 하나 값은 임의 값이 될 수 있습니다.
let product = { price: 5, quantity: 2 }
const targetMap = new WeakMap()
targetMap.set(product, "example code to test")
console.log(targetMap.get(product)) // ---> "example code to test"
  • 객체 대상을 고려하는 targetMapWeakMap 으로 생성한다.
  • track, trigger 실행할때 어떤 타겟 객체인지 알아야 한다. 그래서 target, key 둘다 보내서 확인한다.
const targetMap = new WeakMap()

function track(target, key) {
  let depsMap = targetMap.get(target) // depsMap(객체) 구하기

  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  let dep = depsMap.get(key) // 객체의 속성값을 이용해 effect가 담긴 저장소 가져오기
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }

  dep.add(effect) // effect를 저장소에 추가하기
}

function trigger(target, key) {
  const depsMap = targetMap.get(target) // targetMap에 effect 실행할 객체 가져오기
  if (!depsMap) {
    return
  }

  let dep = depsMap.get(key) // 실행한 객체 속성이 있는 경우
  if (dep) {
    dep.forEach(effect => {
      // 모두 실행
      effect()
    })
  }
}

let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
  total = product.price * product.quantity
}

track(product, 'quantity')
effect()
console.log(total) // --> 10

product.quantity = 3
trigger(product, 'quantity')
console.log(total) // --> 15
  • 여러 객체에 대한 종속성을 추적하는 방법을 알아봤다.
  • 다음에는 track, trigger 호출할때 ES6 proxy를 사용하는 법을 알아보자.

결론

  • Vue2 일때도 잘 몰랐던 내용인데 이번 계기로 자세히 알아보자.
  • 아무 생각없이 잘 변경되는구나...라고 생각했었는데 이런 내부 로직을 알게되니 다양하게 사용이 가능할 것 같다.
728x90
반응형
LIST