Vue 3의 반응형 시스템은 ES2015 Proxy를 기반으로 합니다. 데이터를 읽으면 의존성을 추적(track)하고, 변경하면 효과를 실행(trigger)하는 것이 전부입니다.

면접에서 "Vue의 반응형 시스템이 어떻게 동작하나요?"라는 질문은 시니어급 면접에서 나올 수 있습니다. Proxy의 get/set 트랩과 의존성 추적의 흐름을 설명할 수 있어야 합니다.


반응형의 핵심 개념

PLAINTEXT
1. Proxy의 get 트랩 → 의존성 추적 (track)
2. Proxy의 set 트랩 → 효과 실행 (trigger)
3. effect — 반응형 데이터를 사용하는 함수 (컴포넌트 렌더, computed, watch 등)

간소화된 구현

TYPESCRIPT
// 의존성을 저장하는 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의 내부 구현

TYPESCRIPT
// 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

TYPESCRIPT
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    // 반응하지 않음!

**사용 시나리오 **: 대량 데이터(수천 개 객체 배열)에서 깊은 반응형이 불필요할 때 성능 최적화에 사용합니다.


반응형 디버깅

VUE
<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로 디버깅할 수 있습니다.

댓글 로딩 중...