Vue3은 변화를 어떻게 추적하여 반응할까요?
공식문서의 번역입니다. https://vuejs.org/guide/extras/reactivity-in-depth.html
What is Reactivity? (반응성이 뭔데?)
여기서 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
변수는 이름표이고, 이름표는 이름표가 아닌 이름표가 가리키는 값을 참조하기 때문입니다.
우리는 A1혹은 A0이 변경될 때, A2를 갱신하는 함수를 자동으로 호출하도록 해야 합니다.
자바스크립트로는 어떻게 할까요? 먼저 A2를 업데이트하는 코드를 다시 실행하기 위해 함수로 래핑해 보겠습니다.
let A2
function update() {
A2 = A0 + A1
}
- update() 함수는 프로그램의 상태를 수정하기 때문에 부수 작용 또는 줄여서 효과를 생성합니다.
- A0 및 A1은 해당 값이 효과를 수행하는 데 사용되므로 효과의 의존성으로 간주됩니다. 효과는 의존성에 대한 구독자라고 합니다.
whenDepsChange(update)
효과를 변수의 구독자로 만듭니다.
- 변수를 읽을 때 추적합니다. 예를 들어 표현식 A0 + A1을 평가할 때 A0과 A1이 모두 읽힙니다.
- 현재 실행 중인 효과가 있을 때 변수를 읽으면 해당 효과를 해당 변수의 구독자로 만듭니다.
- 예를 들어 A0과 A1은 update()가 실행될 때 읽히기 때문에 update()는 첫 번째 호출 후에 A0과 A1 모두의 구독자가 됩니다.
- 변수가 변경될 때 감지합니다. 예를 들어 A0에 새 값이 할당되면 모든 해당 변수의 구독자 효과를 다시 실행하도록 알립니다.
How Reactivity Works in Vue(뷰에서 반응성이 동작하는 방식)
JavaScript에서 속성 액세스를 가로채는 방법에는 getter / setters와 Proxies의 두 가지가 있습니다.
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
}
팁
여기와 아래의 코드 조각은 가능한 가장 간단한 형태로 핵심 개념을 설명하기 위한 것이므로
많은 세부 사항이 생략되고 엣지 케이스는 무시합니다.
- 리액티브 객체의 속성을 로컬 변수에 할당하거나 구조 분해하면 로컬 변수에 대한 액세스가 더 이상 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)
}
}
주 : 컴포넌트가 생성되고 사라지기 전까지 반응형 객체는 싱글턴
trigger() 내부에서 속성에 대한 구독자 효과를 다시 조회합니다.
그러나 이번에는 조회 후 해당 효과 함수를 호출합니다.
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
주 : 마운트 전에 반드시 한번 호출되게 되어있음
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++
Runtime vs. Compile-time Reactivity
주 : 객체 속성의 지역변수 할당, 구조분해 할당
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
<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
컴퓨티드 프로터티 자체에 디버깅 콜백을 등록할 수 있습니다.
onTrack 및 onTrigger 콜백이 있는 두 번째 옵션 객체에 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
watch(source, callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
watchEffect(callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
onTrack 및 onTrigger watcher 옵션은 개발 모드에서만 동작합니다.
외부 시스템과 연계하기
불변 데이터
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]
}
상태 기계
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]
}
RxJS
RxJS는 비동기 이벤트 스트림 작업을 위한 라이브러리입니다.
VueUse 라이브러리는 RxJS 스트림을 Vue의 반응성 시스템과 연결하기 위한 @vueuse/rxjs 추가 함수를 제공합니다.
참고
prop 변경은 자연스럽게 하위 컴포넌트를 리렌더링 합니다.
https://itchallenger.tistory.com/793
'FrontEnd' 카테고리의 다른 글
Vue3과 React 훅의 반응성 비교 : 불변 VS 가변 (0) | 2022.10.06 |
---|---|
Vue3의 렌더링 메커니즘 알아보기 (0) | 2022.10.06 |
RxJS와 반응형 프로그래밍 (0) | 2022.10.06 |
디자인시스템 토큰 이름붙이기 [Naming Tokens in Design Systems] (0) | 2022.10.05 |
Vue.js 내부 들여다보기[Demystifying Vue.js internals] (0) | 2022.10.05 |