본문 바로가기

FrontEnd

Vue3 Reactivity In Depth (뷰3의 반응성 원리 이해하기)

반응형

Vue3은 변화를 어떻게 추적하여 반응할까요?

공식문서의 번역입니다. https://vuejs.org/guide/extras/reactivity-in-depth.html

 

Reactivity in Depth | Vue.js

Less than 48 hours to get 45% off at Vue School Access 800+ lessons including the Vue.js 3 Masterclass

vuejs.org

Vue의 가장 독특한 기능 중 하나는 거슬리지 않는 반응성 시스템입니다.
컴포넌트 상태는 반응형 JavaScript 객체로 합성됩니다.
해당 상태를 수정하면 뷰가 업데이트됩니다.
상태 관리를 간단하고 직관적으로 만들지만, 일반적인 문제를 피하기 위해 동작 방식을 이해하는 것도 중요합니다.
이 섹션에서는 Vue의 반응성 시스템의 세부 사항을 파헤칠 것입니다.

What is Reactivity? (반응성이 뭔데?)

이 용어는 요즘 프로그래밍에서 꽤 많이 등장합니다만, 정확하게 무엇을 의미하는거죠?
반응성은 선언적 방법으로 변경 사항을 적용할 수 있게 해주는 프로그래밍 패러다임입니다.
사람들이 일반적으로 보여주는 표준 예는 Excel 스프레드시트입니다.
 
A2의 값은 A1+A2의 결과인 3이 됩니다.

여기서 A2 셀은 = A0 + A1의 수식을 통해 정의되므로(A2를 클릭하여 수식을 보거나 편집할 수 있음) 스프레드시트는 3을 제공합니다.
아무것도 놀랍지 않습니다.
그러나 A0 또는 A1을 업데이트하면 A2도 자동으로 업데이트됨을 알 수 있습니다.

 

JavaScript는 일반적으로 이렇게 동작하지 않습니다.
JavaScript로 비슷한 것을 작성한다면:

let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // Still 3
A0을 변경하면 A2는 자동으로 변경되지 않습니다.
변수는 이름표이고, 이름표는 이름표가 아닌 이름표가 가리키는 값을 참조하기 때문입니다.

우리는 A1혹은 A0이 변경될 때, A2를 갱신하는 함수를 자동으로 호출하도록 해야 합니다.
자바스크립트로는 어떻게 할까요? 먼저 A2를 업데이트하는 코드를 다시 실행하기 위해 함수로 래핑해 보겠습니다.

let A2

function update() {
  A2 = A0 + A1
}
그런 다음 몇 가지 용어를 정의해야 합니다.
  • update() 함수는 프로그램의 상태를 수정하기 때문에 부수 작용 또는 줄여서 효과를 생성합니다.
  • A0 및 A1은 해당 값이 효과를 수행하는 데 사용되므로 효과의 의존성으로 간주됩니다. 효과는 의존성에 대한 구독자라고 합니다.
우리에게 필요한 것은 A0 또는 A1(의존성으로)이 변경될 때마다 update()(효과)를 호출할 수 있는 마법의 함수입니다.
whenDepsChange(update)
이 whenDepsChange() 함수는 다음 작업을 수행합니다.
효과를 변수의 구독자로 만듭니다.
  1. 변수를 읽을 때 추적합니다. 예를 들어 표현식 A0 + A1을 평가할 때 A0과 A1이 모두 읽힙니다.
  2. 현재 실행 중인 효과가 있을 때 변수를 읽으면 해당 효과를 해당 변수의 구독자로 만듭니다.
    1. 예를 들어 A0과 A1은 update()가 실행될 때 읽히기 때문에 update()는 첫 번째 호출 후에 A0과 A1 모두의 구독자가 됩니다.
  3. 변수가 변경될 때 감지합니다. 예를 들어 A0에 새 값이 할당되면 모든 해당 변수의 구독자 효과를 다시 실행하도록 알립니다.

How Reactivity Works in Vue(뷰에서 반응성이 동작하는 방식)

예제에서와 같이 지역 변수의 읽기 및 쓰기를 추적할 수 없습니다.
바닐라 JavaScript에서는 그런 메커니즘이 없습니다.
하지만 우리는 객체 속성의 읽기와 쓰기를 가로챌 수 있습니다.

JavaScript에서 속성 액세스를 가로채는 방법에는  getter / settersProxies의 두 가지가 있습니다.
Vue 2는 브라우저 지원 제한으로 인해 getter/setter만을 사용했습니다.
Vue 3에서 프록시는 리액티브 객체에 사용되고 getter/setter는 참조에 사용됩니다.
다음은 작동 방식을 보여주는 의사 코드입니다.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

여기와 아래의 코드 조각은 가능한 가장 간단한 형태로 핵심 개념을 설명하기 위한 것이므로
많은 세부 사항이 생략되고 엣지 케이스는 무시합니다.
이것은 기본 섹션에서 논의한 반응형 객체의 몇 가지 제한 사항(limitations of reactive objects)을 설명합니다.
  • 리액티브 객체의 속성을 로컬 변수에 할당하거나 구조 분해하면 로컬 변수에 대한 액세스가 더 이상 get/set 프록시 트랩을 트리거하지 않기 때문에 반응성이 "연결 해제"됩니다.
  • Reactive()에서 반환된 프록시는 원본처럼 동작하지만 === 연산자를 사용하여 원본과 비교하면 다른 ID를 갖습니다.

track() 내부에서 현재 실행 중인 효과가 있는지 확인합니다.
존재하는 경우 추적 중인 속성을 구독하는 효과집합(Set)를 조회하고 효과를 Set에 추가합니다.

// This will be set right before an effect is about
// to be run. We'll deal with this later.
let activeEffect

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}
효과 구독은 전역 WeakMap<target, Map<key, Set<effect>>> 데이터 구조에 저장됩니다.
속성에 대한 구독 효과 Set이 발견되지 않은 경우(처음으로 추적됨) 초기화 합니다.
이것은 getSubscribersForProperty() 함수가 하는 일입니다.
간단히 하기 위해 자세한 내용은 건너뛰겠습니다.
주 : 컴포넌트가 생성되고 사라지기 전까지 반응형 객체는 싱글턴

trigger() 내부에서 속성에 대한 구독자 효과를 다시 조회합니다.
그러나 이번에는 조회 후 해당 효과 함수를 호출합니다.

function trigger(target, key) {
  const effects = getSubscribersForProperty(target, key)
  effects.forEach((effect) => effect())
}
이제 whenDepsChange() 함수로 돌아가 보겠습니다.
주 : 마운트 전에 반드시 한번 호출되게 되어있음
function whenDepsChange(update) {
  const effect = () => {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

실제 업데이트를 실행하기 전에 자신을 현재 활성 효과로 설정하는 효과에 파라미터로 전달한 update 함수를 래핑합니다.
이것은 현재 activeEffect를 찾기 위해 업데이트 동안 track() 호출을 활성화합니다.

이제 의존성을 자동으로 추적하고 종속성이 변경될 때마다 다시 실행하는 효과를 만들었습니다.
우리는 이것을 반응 효과(Reactive Effect)라고 부릅니다.

1. 컴포넌트 초기 렌더링 시 업데이트 호출 > 2. A2 데이터 설정 및 의존성 Setup (track) > 3. 나중에 A1과 A2 업데이트는 A3을 변경 (trigger)

Vue는 반응 효과를 만들 수 있는 API인 watchEffect()를 제공합니다.
사실, 예제의 마법 같은 whenDepsChange()와 매우 유사하게 작동한다는 것을 눈치채셨을 것입니다.
이제 실제 Vue API를 사용하여 원래 예제를 다시 작업할 수 있습니다.

import { ref, watchEffect } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()

watchEffect(() => {
  // tracks A0 and A1
  A2.value = A0.value + A1.value
})

// triggers the effect
A0.value = 2

반응 효과를 사용하여 ref를 변경하는 것은 흥미로운 사용 사례가 아닙니다.
사실 computed property를 사용하면 더 선언적입니다.

import { ref, computed } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)

A0.value = 2

내부적으로 computed는 무효화 및 재계산을 반응 효과를 사용하여 관리합니다.

그렇다면 일반적이고 유용한 반응 효과의 예는 무엇입니까?
DOM 업데이트 입니다.
다음과 같이 간단한 "반응형 렌더링"을 구현할 수 있습니다.

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `count is: ${count.value}`
})

// updates the DOM
count.value++
사실 이것은 Vue 컴포넌트가 상태와 DOM을 동기화 상태로 유지하는 방법에 매우 가깝습니다.
각 컴포넌트 인스턴스는 DOM을 렌더링하고 업데이트하는 반응 효과를 생성합니다.
물론 Vue 컴포넌트는 innerHTML보다 DOM을 업데이트하는 데 훨씬 더 효율적인 방법을 사용합니다.
이것은 Rendering Mechanism에서 설명합니다.

Runtime vs. Compile-time Reactivity

Vue의 반응성 시스템은 주로 런타임 기반입니다.
추적 및 트리거링은 코드가 브라우저에서 직접 실행되는 동안 모두 수행됩니다.
런타임 반응성의 장점은 빌드 단계 없이 작동할 수 있고 엣지 케이스가 적다는 것입니다.
반면에 이것은 JavaScript의 구문에 의해 제약을 받습니다.
이전 예제에서 이미 한계에 부딪쳤습니다.
JavaScript는 지역 변수의 읽기 및 쓰기를 가로챌 수 있는 방법을 제공하지 않으므로
주 : 객체 속성의 지역변수 할당, 구조분해 할당
 
항상 반응 객체 또는 참조를 사용하여 객체 속성으로 반응 상태에 액세스해야 합니다.
코드의 장황함을 줄이기 위해 Reactivity Transform 기능을 실험했습니다.
let A0 = $ref(0)
let A1 = $ref(1)

// track on variable read
const A2 = $computed(() => A0 + A1)

// trigger on variable write
A0 = 2

이 스니펫은 변수에 대한 참조 뒤에 자동으로 .value를 추가하여 변환 없이 작성한 것과 정확히 일치하도록 컴파일합니다.
Reactivity Transform을 사용하면 Vue의 반응성 시스템이 하이브리드(컴파일+런타임) 시스템이 됩니다.


Reactivity Debugging

Vue의 반응성 시스템이 종속성을 자동으로 추적하는 것은 좋지만
경우에 따라 추적 중인 항목 또는 컴포넌트를 다시 렌더링하는 원인을 정확히 파악해야 할 수도 있습니다.

Component Debugging

컴포넌트의 렌더링 중에 사용되는 의존성과
onRenderTracked 및 onRenderTriggered 생명주기 훅을 사용하여 업데이트를 트리거하는 의존성을 디버그할 수 있습니다.
두 훅 모두 해당 의존성에 대한 정보가 포함된 디버거 이벤트를 수신합니다.
의존성을 대화식으로 검사하기 위해 콜백에 디버거 문을 배치하는 것이 좋습니다.
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})
</script>
컴포넌트 디버깅 훅은 개발 모드에서만 작동합니다.
디버그 이벤트 객체의 타입은 다음과 같습니다.
type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type:
    | TrackOpTypes /* 'get' | 'has' | 'iterate' */
    | TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
  key: any
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

Computed Debugging

컴퓨티드 프로터티 자체에 디버깅 콜백을 등록할 수 있습니다.

onTrackonTrigger 콜백이 있는 두 번째 옵션 객체에 computed()를 전달하여 계산된 속성을 디버그할 수 있습니다.

  • onTrack은 반응 속성 또는 참조가 의존성으로 추적될 때 호출됩니다.
  • onTrigger는 watcher 콜백이 의존성의 돌연변이에 의해 트리거될 때 호출됩니다.
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // triggered when count.value is tracked as a dependency
    debugger
  },
  onTrigger(e) {
    // triggered when count.value is mutated
    debugger
  }
})

// access plusOne, should trigger onTrack
console.log(plusOne.value)

// mutate count.value, should trigger onTrigger
count.value++

 

onTrack 및 onTrigger 옵션은 개발 모드에서만 동작합니다.

Watcher Debugging

computed()와 유사하게 watcher도 onTrackonTrigger 옵션도 지원합니다.
watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})
onTrack 및 onTrigger watcher 옵션은 개발 모드에서만 동작합니다.

외부 시스템과 연계하기

Vue의 반응성 시스템은 일반 JavaScript 객체를 반응성 프록시로 깊은 변환을 수행하여 동작합니다.
이 깊은 변환은 외부 상태 관리 시스템과 통합할 때 필요하지 않을 수 있습니다.
(예: 외부 솔루션도 프록시를 사용하는 경우).
 
Vue의 반응성 시스템을 외부 상태 관리 솔루션과 통합하는 일반적인 아이디어는 외부 상태를 shallowRef에 유지하는 것입니다.
얕은 참조는 .value 속성에 액세스할 때만 반응합니다. 내부 값은 그대로 유지됩니다.
외부 상태가 변경되면 ref 값을 교체하여 업데이트를 트리거합니다.

불변 데이터

실행 취소 / 다시 실행 기능을 구현하는 경우 사용자가 편집할 때마다 애플리케이션 상태의 스냅샷을 찍고 싶을 것입니다.
그러나 Vue의 가변 반응성 시스템은 상태 트리가 큰 경우 가장 적합하지 않습니다.
모든 업데이트에서 전체 상태 객체를 직렬화하는 것은 CPU 및 메모리 비용 측면에서 비용이 많이 들 수 있기 때문입니다.
불변 데이터 구조(Immutable data structures)는 상태 객체를 절대 변경하지 않음으로써 이 문제를 해결합니다.
대신 이전 객체와 동일하고 변경되지 않은 부분을 공유하는 새 객체를 생성합니다.
JavaScript에서 변경할 수 없는 데이터를 사용하는 방법에는 여러 가지가 있지만 Vue와 함께 Immer를 사용하는 것이 좋습니다.
Immer를 사용하면 보다 인체공학적이고 변경 가능한 구문을 유지하면서 변경할 수 없는 데이터를 사용할 수 있기 때문입니다.
간단한 컴포넌트를 통해 Immer를 Vue와 통합할 수 있습니다.
import produce from 'immer'
import { shallowRef } from 'vue'

export function useImmer(baseState) {
  const state = shallowRef(baseState)
  const update = (updater) => {
    state.value = produce(state.value, updater)
  }

  return [state, update]
}

Try it in the Playground

상태 기계

State Machine은 애플리케이션이 있을 수 있는 모든 가능한 상태와 한 상태에서 다른 상태로 전환할 수 있는 모든 가능한 방법을 설명하는 모델입니다.
단순한 컴포넌트에는 과도할 수 있지만 복잡한 상태 흐름을 보다 강력하고 관리하기 쉽게 만드는 데 도움이 될 수 있습니다.
JavaScript에서 가장 널리 사용되는 상태 머신 구현 중 하나는 XState입니다. 다음은 이에 통합되는 컴포저블입니다.
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'

export function useMachine(options) {
  const machine = createMachine(options)
  const state = shallowRef(machine.initialState)
  const service = interpret(machine)
    .onTransition((newState) => (state.value = newState))
    .start()
  const send = (event) => service.send(event)

  return [state, send]
}

Try it in the Playground

RxJS

RxJS는 비동기 이벤트 스트림 작업을 위한 라이브러리입니다.
VueUse 라이브러리는 RxJS 스트림을 Vue의 반응성 시스템과 연결하기 위한 @vueuse/rxjs 추가 함수를 제공합니다.

참고

prop 변경은 자연스럽게 하위 컴포넌트를 리렌더링 합니다.

https://sfc.vuejs.org/#eNqFUbFOwzAQ/ZXDS1qptcUa0kqIhRGJgcVLaS9pqtg+2U4RsvzvnJO2AgbYfO/dvbv3nMQjkTyPKGrRhL3vKULAONJW296Q8xESeGwhQ+udgYpbqxv15AxdcKlKUZSY3jsbIpjQwaYML6pnHAYHb84Ph7tqqW2j5l28hYuIhoZdRK4Amt7SGOG8Nu6Aw0YLltFipqZ9NQNXWDHeqNu8WIn5srXZkTwFZ9lWKqP6QgQtapiQgvG1pdbiGCOFWqnQ7ouFU5DOd4pf0o829gYlBrN+9+4joGdhLYpE1jbzyqvxfyJcHbDtLb54R+FXnHNeNDEb+Na3SGyzTvGTsH6NvrddzsuHKbQ/Ejzeb1Oa5WT5hJwbxdjPqPIXEIqxWA== 

 

Vue SFC Playground

 

sfc.vuejs.org

https://itchallenger.tistory.com/793

 

vue3의 반응성(reactivity) 원리 간단 정리

출처 : https://vuejs.org/guide/extras/reactivity-in-depth.html Reactivity in Depth | Vue.js vuejs.org 아래와 같은 코드가 실행된다 가정하자 import { ref, watchEffect } from 'vue' const A0 = ref(0) const A1 = ref(1) const A2 = ref() watchEf

itchallenger.tistory.com

 

반응형