본문 바로가기

FrontEnd

Vue3의 반응성 완벽 이해 2편 : ref와 computed

반응형

Vue3의 반응성을 직접 구현하며 Vue3에 대한 더 깊은 이해를 도모해 봅시다.

이전 시리즈 :

2022.12.10 - [Vue.js] - Vue3의 반응성 완벽 이해 1편 : 종속성 추적

 

Vue3의 반응성 완벽 이해 1편 : 종속성 추적

vue mastery의 리액트 코어팀이 설명해주는 vue3의 반응성의 원리를 알아봅니다. 공식 문서와 nhn의 게시물을 읽어봤지만, 약간 납득이 안되는 부분들이 있었습니다. 해당 강의를 통해 어느정도 해소

itchallenger.tistory.com

vue3

실행 중인 효과를 저장하는 전역 변수와 ref

이전 게시물의 예제 소스 코드(all-together.js)의 동작에는 하나의 문제점이 있는데요,

이전 소스 코드 실행 후 get을 실행할 때마다 effect가 추가됩니다.

(주 : 전역에 effect가 하나 존재하며, track 시마다 해당 함수를)

  • 우리는 effect가 실행중일 때에만 get 호출 시 구독자 함수들을 실행하고 싶습니다.
  • 또한 여러 개의 effect를 등록하고 싶습니다.

(주 : 종속성은 대상 객체에 대해 생성돠는 객체(사실 weakMap)이며, 구독자들을 관리함)

전역 변수 activeEffect

이 문제를 해결하기 위해 먼저 현재 실행 중인 효과를 저장할 전역 변수인 activeEffect를 만듭니다.

그런 다음 effect라는 새 함수 내에서 해당 전역 변수를 설정합니다.

let activeEffect = null // The active effect running
...
function effect(eff) {
  activeEffect = eff  // Set this as the activeEffect
  activeEffect()      // Run it
  activeEffect = null // Unset it
}

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

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

effect(() => {
  salePrice = product.price * 0.9
})

console.log(
  `Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`
)

product.quantity = 3

console.log(
  `After updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`
)

product.price = 10

console.log(
  `After updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`
)

이제, 더 이상 효과를 수동으로 호출할 필요가 없습니다. (effect 함수가 대신 실행해줌)

이전 소스코드의 track 함수를 effect가 실행 중일 때만 호출하도록 변경합니다.

function track(target, key) {
  if (activeEffect) { // <------ Check to see if we have an activeEffect
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map())) 
    }
    let dep = depsMap.get(key) 
    if (!dep) {
      depsMap.set(key, (dep = new Set())) // Create a new Set
    }
    dep.add(activeEffect) // <----- Add activeEffect to dependency map
  }
}

ref 함수

지금까지는 반응성을 가진 객체를 만들었습니다.

하지만 반응성을 가진 단일 값을 만들려먼 어떻게 할까요?

아쉽게도 프록시는 객체에 대해서 동작합니다.

따라서 단일 값을 객체에 집어넣어야 합니다.

 

이를 대신해주는 함수가 ref입니다.

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0

기존 소스코드를 활용한다면 이런 식으로 구현이 가능합니다.

function ref(intialValue) {
  return reactive({ value: initialValue })
}

그러나 이것은 Vue 3가 프리미티브로 ref를 정의하는 방식이 아니므로 다르게 구현해 보겠습니다.

자바스크립트 객체 접근자

먼저 JavaScript Object Accessors(자바스크립트 객체 접근자)에 대한 이해가 필요합니다.

이는 JavaScript computed 속성이라고도 합니다(Vue computed 속성과 혼동하지 말 것).

아래에서 객체 접근자를 사용하는 간단한 예를 볼 수 있습니다.

let user = {
  firstName: 'Gregg',
  lastName: 'Pollack',

  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ')
  },
}

console.log(`Name is ${user.fullName}`)
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`)

get 및 set 행은 fullName을 가져오고 그에 따라 fullName을 설정하는 개체 접근자입니다.

이것은 일반 JavaScript이며 Vue의 기능이 아닙니다.

객체 접근자로 Ref 정의

track 및 trigger 함수와 함께 객체 접근자를 사용하여 ref를 정의할 수 있습니다.

이게 끝입니다.

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

아래 코드는 우리가 Vue3에서 사용하는 것과 동일합니다.

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0

effect(() => {
  salePrice.value = product.price * 0.9
})

effect(() => {
  total = salePrice.value * product.quantity
})

console.log(
  `Before updated quantity total (should be 9) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
  `After updated quantity total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
  `After updated price total (should be 27) = ${total} salePrice (should be 9) = ${salePrice.value}`
)

출력 결과는 우리가 원한 그대로 입니다.

Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated total (should be 27) = 27 salePrice (should be 9) = 9

우리의 salePrice는 이제 반응적이며 변경될 때 total이 업데이트됩니다!

Computed Value

지금까지 Vue3의 Reactivity 모듈에 대해 학습했습니다.

위의 코드 구조는 실제 Vue3의 코드베이스에 대응합니다.

Vue3 Codebase

이전 예제에서 다룬 total과 salesPrice는

vue3를 할 줄 안다면 computed로 구현할 것입니다.

let product = reactive({ price: 5, quantity: 2 })

let salePrice = computed(() => {
  return product.price * 0.9
})
let total = computed(() => {
  return salePrice.value * product.quantity
})

이제 computed 함수를 구현해 봅시다.

ref 객체를 새로 만든 다음, effect가 호출될 때마다 ref의 값을 업데이트 해 주는 것이 전부입니다.

effect는 getter 

function computed(getter) {
  let result = ref()  // Create a new reactive reference

  effect(() => (result.value = getter())) // Set this value equal to the return value of the getter

  return result // return the reactive reference
}

먼저 effect 함수 내부의 익명 함수 a (()=>result.vaule=getter())에 대해

  1. effect가 실행되면 activeEffect에 익명 함수 a가 설정됨.
  2. effect 내부에서 반응형 객체에 접근하면 프록시의 트랩이 호출됨
    • 프록시 트랩은 어떤 객체의 어떤 키가 접근되었는지 알 수 있음
    • getter 내부의 객체들이 target
  3. 프록시의 트랩 내에서 track 함수가 호출되며 activeEffect(a)를 해당 속성에 대한 구독자로 등록함
    • getter 내부 객체들의 각 프로퍼티에 대한 effect(a)가 등록됨
  4. 새로 만든 result객체의 value 프로퍼티에 대해서도 activeEffect (a)가 등록됨
  5. getter 내부 객체들의 속성 값이 변경되면 a가 다시 실행
    • 이는 ref.value의 값을 바꿈
    • 따라서 computed의 값이 바뀜!

이제 이 소스코드(on Github)를 실행해 보세요!

반응 객체에 새로운 속성 추가하기

Vue 2에서는 불가능했던 반응 객체로 무언가를 할 수 있다는 점을 언급할 가치가 있습니다.

특히 새로운 반응 속성을 추가할 수 있습니다.

 

Vue 2에서는 Object.defineProperty를 사용하여 getter 및 setter를 개별 개체 속성에 추가하는,

반응성이 구현된 방식 때문에 불가능했습니다.

이제 프록시를 사용하면 문제 없이 새 속성을 추가할 수 있으며 즉시 반응합니다.

 

즉 아래 예제는

...
let product = reactive({ price: 5, quantity: 2 })
...

product.name = 'Shoes'
effect(() => {
  console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'

다음과 같이 출력합니다.

Product name is now Shoes
Product name is now Socks

다음 게시물에서는 Vue3의 Reactivty 디자인에 대한 에반 유의 설명을 살펴보고,

실제 Vue3 코드베이스를 읽어보겠습니다.

반응형