반응형 시스템 내부
Vue 3의 반응형 시스템은 ES2015 Proxy를 기반으로 합니다. 데이터를 읽으면 의존성을 추적(track)하고, 변경하면 효과를 실행(trigger)하는 것이 전부입니다.
면접에서 "Vue의 반응형 시스템이 어떻게 동작하나요?"라는 질문은 시니어급 면접에서 나올 수 있습니다. Proxy의 get/set 트랩과 의존성 추적의 흐름을 설명할 수 있어야 합니다.
반응형의 핵심 개념
1. Proxy의 get 트랩 → 의존성 추적 (track)
2. Proxy의 set 트랩 → 효과 실행 (trigger)
3. effect — 반응형 데이터를 사용하는 함수 (컴포넌트 렌더, computed, watch 등)
간소화된 구현
// 의존성을 저장하는 WeakMap
// targetMap: WeakMap<object, Map<string, Set<Function>>>
const targetMap = new WeakMap()
let activeEffect: Function | null = null
// 의존성 추적 — get 트랩에서 호출
function track(target: object, key: string) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
// 효과 실행 — set 트랩에서 호출
function trigger(target: object, key: string) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (!deps) return
deps.forEach(effect => effect())
}
// reactive 구현
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key as string) // 읽을 때 추적
return typeof result === 'object' && result !== null
? reactive(result) // 중첩 객체도 반응형으로
: result
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key as string) // 변경 시 트리거
}
return result
}
})
}
// effect 등록
function effect(fn: Function) {
activeEffect = fn
fn() // 최초 실행 — 이때 get이 호출되어 의존성 추적
activeEffect = null
}
Vue 2 vs Vue 3 반응형
| 항목 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 속성 추가 | 감지 불가 ($set 필요) | 자동 감지 |
| 속성 삭제 | 감지 불가 ($delete 필요) | 자동 감지 |
| 배열 인덱스 | 감지 불가 | 자동 감지 |
| Map/Set | 미지원 | 지원 |
| 성능 | 초기화 시 모든 속성 순회 | Lazy (접근 시 변환) |
ref의 내부 구현
// ref는 value 속성 하나만 가진 객체의 getter/setter로 구현
function ref(rawValue: any) {
const r = {
get value() {
track(r, 'value')
return rawValue
},
set value(newValue) {
if (rawValue !== newValue) {
rawValue = newValue
trigger(r, 'value')
}
}
}
return r
}
이것이 ref에서 .value가 필요한 근본적인 이유입니다. JavaScript에서 원시값의 접근을 가로채려면 객체로 감싸야 합니다.
shallowRef와 shallowReactive
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef — .value 변경만 추적, 내부 속성 변경은 무시
const state = shallowRef({ count: 0, nested: { deep: 1 } })
state.value.count = 1 // 반응하지 않음!
state.value = { count: 1, nested: { deep: 1 } } // 이건 반응함
// 수동 트리거
state.value.count = 2
triggerRef(state) // 강제로 업데이트 트리거
// shallowReactive — 최상위 속성만 반응형
const shallow = shallowReactive({ a: 1, nested: { b: 2 } })
shallow.a = 10 // 반응함
shallow.nested.b = 20 // 반응하지 않음!
**사용 시나리오 **: 대량 데이터(수천 개 객체 배열)에서 깊은 반응형이 불필요할 때 성능 최적화에 사용합니다.
반응형 디버깅
<script setup lang="ts">
import { ref, onRenderTracked, onRenderTriggered } from 'vue'
const count = ref(0)
// 개발 모드에서만 동작
onRenderTracked((event) => {
// 어떤 반응형 데이터가 추적되었는지
console.log('추적됨:', event.key, event.type)
})
onRenderTriggered((event) => {
// 어떤 변경이 리렌더링을 트리거했는지
console.log('트리거됨:', event.key, event.type, event.newValue)
debugger // 브레이크포인트
})
</script>
면접 팁
- Proxy의 get/set 트랩 → track/trigger의 흐름을 그림으로 그려 설명하면 인상적입니다
- Vue 2의
$set문제와 Vue 3의 Proxy가 이를 어떻게 해결하는지 대비해서 설명하세요 - Lazy 변환 — Vue 3는 접근 시점에 중첩 객체를 reactive로 변환하므로, 초기화 성능이 Vue 2보다 좋습니다
요약
Vue 3의 반응형 시스템은 Proxy의 get/set 트랩을 통해 의존성 추적(track)과 효과 실행(trigger)을 구현합니다. ref는 .value 속성의 getter/setter로, reactive는 Proxy로 동작합니다. shallowRef/shallowReactive로 성능 최적화가 가능하고, onRenderTracked/onRenderTriggered로 디버깅할 수 있습니다.
댓글 로딩 중...