// Vue 2 방식 (단순화)
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
// 의존성 추적
track(key)
return value
},
set(newValue) {
value = newValue
// 변경 알림
trigger(key)
}
})
}// Vue 2에서 반응형이 동작하지 않는 경우
const state = Vue.observable({ items: [] })
state.items.push('new item') // ← 배열 변경 미감지 (일부 메서드만 패치됨)
state.newProp = 'hello' // ← 새로운 프로퍼티 추가 미감지
delete state.items // ← 삭제 미감지const target = { count: 0 }
const proxy = new Proxy(target, {
get(target, key, receiver) { // ← 프로퍼티 읽기 시 호출
console.log(`get: ${key}`)
return Reflect.get(target, key, receiver) // ← 원래 동작 수행
},
set(target, key, value, receiver) { // ← 프로퍼티 쓰기 시 호출
console.log(`set: ${key} = ${value}`)
return Reflect.set(target, key, value, receiver)
}
})
proxy.count // 출력: "get: count"
proxy.count = 5 // 출력: "set: count = 5"
proxy.newProp = 1 // 출력: "set: newProp = 1" ← 새 프로퍼티도 감지됨const handler = {
get(target, key, receiver) {}, // obj.key
set(target, key, value, receiver) {}, // obj.key = value
has(target, key) {}, // key in obj
deleteProperty(target, key) {}, // delete obj.key
apply(target, thisArg, args) {}, // fn() ← 함수에 적용
construct(target, args) {}, // new Fn() ← 생성자에 적용
ownKeys(target) {}, // Object.keys(obj)
getOwnPropertyDescriptor(target, key) {},
defineProperty(target, key, desc) {},
getPrototypeOf(target) {},
setPrototypeOf(target, proto) {},
isExtensible(target) {},
preventExtensions(target) {},
}targetMap (WeakMap)
target 객체 → depsMap (Map)
프로퍼티 key → dep (Set<effect>)// Vue 3 내부 (단순화)
let activeEffect = null // ← 현재 실행 중인 effect
function get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// activeEffect가 있으면 이 키를 의존성으로 등록
track(target, key)
// targetMap[target][key].add(activeEffect)
return result
}function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 이 키에 등록된 모든 effect를 실행
trigger(target, key)
// targetMap[target][key].forEach(effect => effect())
return result
}import { reactive, effect } from '@vue/reactivity'
const state = reactive({ count: 0 }) // ← Proxy 생성
effect(() => {
// 1. activeEffect = 이 함수로 설정
// 2. state.count 읽기 → get 트랩 → track() 호출
// → targetMap[state]["count"].add(현재 effect)
console.log(state.count)
// 3. activeEffect = null
})
state.count = 1
// → set 트랩 → trigger() 호출
// → targetMap[state]["count"]에 등록된 effect 실행
// → 콘솔에 1 출력1. reactive({ count: 0 }) 호출
→ new Proxy(target, handler) 생성
2. effect(fn) 호출
→ activeEffect = fn
→ fn() 실행
→ state.count 읽기
→ get 트랩 발동
→ track(state, "count")
→ targetMap.get(state).get("count").add(fn)
→ 0 반환
→ activeEffect = null
3. state.count = 1 실행
→ set 트랩 발동
→ Reflect.set으로 원본 업데이트
→ trigger(state, "count")
→ targetMap.get(state).get("count")
→ [fn] 실행
→ 콘솔에 1 출력get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key)
// 반환값이 객체이면 재귀적으로 reactive 적용
if (typeof result === 'object' && result !== null) {
return reactive(result) // ← 이미 Proxy면 캐시에서 반환
}
return result
}
reactive() — 객체를 Proxy로 감싸서 반응형으로 만든다
effect() — 반응형 데이터를 읽는 함수를 등록한다
track() — get 트랩에서 현재 effect를 의존성으로 기록한다
trigger() — set 트랩에서 의존성에 등록된 effect를 실행한다 const state = reactive({ count: 0 })effect(() => {
console.log(state.count) // 이 줄이 실행될 때 track()이 호출됨
})state.count = 1 // 여기서 trigger()
컴포넌트 마운트
→ Vue 내부에서 effect(렌더 함수) 등록
→ 렌더 함수 실행
→ 템플릿에서 count.value 읽힘
→ track() → "렌더 effect ↔ count.value" 연결
// <template><div>{{ count }}</div></template>
// 컴파일 결과
function render() {
return h('div', count.value) // 가상 DOM 생성
}
// 아래 effect에 등록이 된다.
// count.value가 변경되면 trigger → 아래 렌더 함수 재실행 → DOM 업데이트 진행
effect(() => {
const vnode = render() // 가상 DOM 생성
patch(prevVnode, vnode) // 실제 DOM 업데이트
})// React의 방식 — 명시적
setCount(prev => prev + 1)
// 누가, 언제, 어디서 상태를 바꿨는지 명확히 알 수 있다
// Proxy 방식이었다면
count++
// 어디서 변경이 일어났는지 추적하기 어려움// React가 업데이트를 스케줄링하는 방식
startTransition(() => {
setItems(heavyComputation()) // ← 우선순위 낮게 처리
})
// 사용자 입력 업데이트는 즉시 처리
setInputValue(e.target.value)// React.memo, useMemo, useCallback의 동작 원리
const prev = { count: 0 }
const next = { count: 0 }
prev === next // false → 다른 객체이므로 리렌더링
// 불변 업데이트
const updated = { ...prev, count: 1 }
prev === updated // false → 명확히 다름const state = reactive({ count: 0 })
const before = state
state.count++
const after = state
before === after // true ← 같은 Proxy 객체항목 | Vue 3 (Proxy) | React (불변성) |
상태 변경 방식 | 직접 수정 (obj.key = value) | setState / useState |
변경 감지 | Proxy set 트랩 (암묵적) | 참조 비교 (명시적) |
업데이트 스케줄링 | 동기적, 즉시 트리거 | 비동기, 스케줄러 관리 |
메모이제이션 기반 | 의존성 추적 | 참조 동일성 (===) |
중첩 객체 반응형 | 자동 (재귀 Proxy) | 직접 불변 업데이트 필요 |
디버깅 추적 | 암묵적 변경 추적 어려움 | setState 호출 지점 명확 |
동시성 지원 | 어려움 | Concurrent Mode 지원 |
학습 곡선 | 낮음 (직관적) | 불변성 개념 학습 필요 |
function createValidatedUser(initial) {
return new Proxy(initial, {
set(target, key, value) {
if (key === 'age' && typeof value !== 'number') {
throw new TypeError('age는 숫자여야 합니다') // ← 쓰기 시점에 검증
}
if (key === 'email' && !value.includes('@')) {
throw new Error('올바른 이메일 형식이 아닙니다')
}
return Reflect.set(target, key, value)
}
})
}
const user = createValidatedUser({ name: 'Kim', age: 20 })
user.age = 'abc' // ← TypeError 발생
user.email = 'invalid' // ← Error 발생function withLogging(target, label) {
return new Proxy(target, {
get(target, key, receiver) {
console.log(`[${label}] get: ${String(key)}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`[${label}] set: ${String(key)} = ${JSON.stringify(value)}`)
return Reflect.set(target, key, value, receiver)
}
})
}
const config = withLogging({ timeout: 3000 }, 'Config')
config.timeout // 출력: [Config] get: timeout
config.timeout = 5000 // 출력: [Config] set: timeout = 5000const withDefault = (target, defaultValue) =>
new Proxy(target, {
get(target, key) {
return key in target
? Reflect.get(target, key)
: defaultValue // ← 없는 키 접근 시 기본값 반환
}
})
const scores = withDefault({}, 0)
scores.math = 90
console.log(scores.math) // 90
console.log(scores.english) // 0 ← 기본값
