본문으로 건너뛰기

[Hard] 양방향 데이터 바인딩

1. Please explain the underlying principle of how Vue2 and Vue3 each implement two-way binding​

Vue2와 Vue3 각각의 양방향 바인딩 구현 원리를 설명하세요.

Vue의 양방향 바인딩을 이해하려면 먼저 반응형 시스템의 동작 메커니즘과 Vue2와 Vue3의 구현 차이를 이해해야 합니다.

Vue2의 구현 방식​

Vue2는 Object.defineProperty를 사용하여 양방향 바인딩을 구현합니다. 이 메서드는 객체의 속성을 getter와 setter로 래핑하여 객체 속성의 변화를 감시할 수 있습니다. 흐름은 다음과 같습니다:

1. Data Hijacking (데이터 하이재킹)​

Vue2에서 컴포넌트의 데이터 객체가 생성될 때, Vue는 객체의 모든 속성을 순회하며 Object.defineProperty를 사용하여 이 속성들을 getter와 setter로 변환합니다. 이를 통해 Vue가 데이터의 읽기와 수정을 추적할 수 있습니다.

2. Dependency Collection (의존성 수집)​

컴포넌트의 렌더링 함수가 실행될 때, data의 속성을 읽으면 getter가 트리거됩니다. Vue는 이 의존성들을 기록하여 데이터가 변경될 때 해당 데이터에 의존하는 컴포넌트에 알림을 보낼 수 있습니다.

3. Dispatching Updates (업데이트 디스패치)​

데이터가 수정되면 setter가 트리거되고, Vue는 해당 데이터에 의존하는 모든 컴포넌트에 알림을 보내 렌더링 함수를 다시 실행하여 DOM을 업데이트합니다.

Vue2 코드 예제​

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`get ${key}: ${val}`);
return val;
},
set: function reactiveSetter(newVal) {
console.log(`set ${key}: ${newVal}`);
val = newVal;
},
});
}

const data = { name: 'Pitt' };
defineReactive(data, 'name', data.name);

console.log(data.name); // getter 트리거, "get name: Pitt" 출력
data.name = 'Vue2 Reactivity'; // setter 트리거, "set name: Vue2 Reactivity" 출력

Vue2의 한계​

Object.defineProperty 사용에는 몇 가지 한계가 있습니다:

  • 객체 속성의 추가/삭제를 감지할 수 없음: Vue.set() 또는 Vue.delete()를 사용해야 함
  • 배열 인덱스 변화를 감지할 수 없음: Vue가 제공하는 배열 메서드(push, pop 등)를 사용해야 함
  • 성능 문제: 모든 속성을 재귀적으로 순회하여 getter와 setter를 미리 정의해야 함

Vue3의 구현 방식​

Vue3는 ES6의 Proxy를 도입했습니다. 이 메서드는 객체를 프록시로 래핑하여 객체 속성의 변화를 감시할 수 있으며, 동시에 성능이 더 최적화되었습니다. 흐름은 다음과 같습니다:

1. Proxy를 사용한 데이터 하이재킹​

Vue3에서는 new Proxy를 사용하여 데이터에 대한 프록시를 생성합니다. 각 속성마다 getter와 setter를 정의하는 대신, 더 세밀한 수준에서 데이터 변화를 추적하고 속성 추가/삭제 등 더 많은 유형의 작업을 가로챌 수 있습니다.

2. 더 효율적인 의존성 추적​

Proxy를 사용함으로써 Vue3는 더 효율적으로 의존성을 추적할 수 있습니다. getter/setter를 미리 정의할 필요가 없고, Proxy의 가로채기 능력이 더 강력하여 최대 13가지 작업(get, set, has, deleteProperty 등)을 가로챌 수 있습니다.

3. 자동 최소화 재렌더링​

데이터가 변경될 때, Vue3는 UI의 어떤 부분이 업데이트되어야 하는지 더 정확하게 판단할 수 있어 불필요한 재렌더링을 줄이고 성능을 향상시킵니다.

Vue3 코드 예제​

function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
console.log(`가져오기 ${key}: ${result}`);
return result;
},
set(target, key, value, receiver) {
const success = Reflect.set(target, key, value, receiver);
console.log(`설정 ${key}: ${value}`);
return success;
},
};

return new Proxy(target, handler);
}

const data = reactive({ name: 'Vue 3' });

console.log(data.name); // 데이터 읽기, Proxy의 get 트리거, "가져오기 name: Vue 3" 출력
data.name = 'Vue 3 Reactivity'; // 데이터 수정, Proxy의 set 트리거, "설정 name: Vue 3 Reactivity" 출력
console.log(data.name); // "가져오기 name: Vue 3 Reactivity" 출력

Vue2 vs Vue3 비교표​

특성Vue2Vue3
구현 방식Object.definePropertyProxy
속성 추가 감지❌ Vue.set() 필요✅ 네이티브 지원
속성 삭제 감지❌ Vue.delete() 필요✅ 네이티브 지원
배열 인덱스 감지❌ 특정 메서드 필요✅ 네이티브 지원
성능모든 속성을 재귀적으로 순회지연 처리, 성능이 더 좋음
브라우저 지원IE9+IE11 지원 안 함

결론​

Vue2는 Object.defineProperty를 사용하여 양방향 바인딩을 구현하지만, 이 방법에는 일정한 한계가 있습니다(예: 객체의 속성 추가/삭제를 감지할 수 없음). Vue3는 ES6의 Proxy를 도입하여 더 강력하고 유연한 반응형 시스템을 제공하며 성능도 향상시켰습니다. 이것은 Vue3의 Vue2 대비 주요 개선 사항 중 하나입니다.

2. Why does Vue3 use Proxy instead of Object.defineProperty?​

Vue3는 왜 Object.defineProperty 대신 Proxy를 사용하는가?

주요 이유​

1. 더 강력한 가로채기 능력​

Proxy는 최대 13가지 작업을 가로챌 수 있지만, Object.defineProperty는 속성의 읽기와 설정만 가로챌 수 있습니다:

// Proxy가 가로챌 수 있는 작업
const handler = {
get() {}, // 속성 읽기
set() {}, // 속성 설정
has() {}, // in 연산자
deleteProperty() {}, // delete 연산자
ownKeys() {}, // Object.keys()
getOwnPropertyDescriptor() {},
defineProperty() {},
preventExtensions() {},
getPrototypeOf() {},
isExtensible() {},
setPrototypeOf() {},
apply() {}, // 함수 호출
construct() {} // new 연산자
};

2. 배열 인덱스 모니터링 네이티브 지원​

// Vue2에서 감지 불가
const arr = [1, 2, 3];
arr[0] = 10; // ❌ 업데이트 트리거 불가

// Vue3에서 감지 가능
const arr = reactive([1, 2, 3]);
arr[0] = 10; // ✅ 업데이트 트리거 가능

3. 객체 속성 동적 추가/삭제 네이티브 지원​

// Vue2에서는 특별한 처리 필요
Vue.set(obj, 'newKey', 'value'); // ✅
obj.newKey = 'value'; // ❌ 업데이트 트리거 불가

// Vue3에서는 네이티브 지원
const obj = reactive({});
obj.newKey = 'value'; // ✅ 업데이트 트리거 가능
delete obj.newKey; // ✅ 업데이트 트리거 가능

4. 더 나은 성능​

// Vue2: 모든 속성을 재귀적으로 순회 필요
function observe(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
// 값이 객체이면 재귀 처리 필요
if (typeof obj[key] === 'object') {
observe(obj[key]);
}
});
}

// Vue3: 지연 처리, 접근 시에만 프록시 생성
function reactive(obj) {
return new Proxy(obj, handler); // 재귀 불필요
}

5. 더 간결한 코드​

Vue3의 반응형 구현 코드량이 대폭 감소하여 유지보수 비용이 낮아졌습니다.

Vue2가 Proxy를 사용하지 않은 이유​

주된 이유는 브라우저 호환성입니다:

  • Vue2 출시 당시(2016년), Proxy는 아직 널리 지원되지 않았음
  • Vue2는 IE9+를 지원해야 했고, Proxy는 polyfill이 불가능
  • Vue3는 IE11 지원을 포기하여 Proxy를 채택 가능

실제 예제 비교​

// ===== Vue2의 한계 =====
const vm = new Vue({
data: {
obj: { a: 1 },
arr: [1, 2, 3]
}
});

// ❌ 다음 작업은 업데이트를 트리거하지 않음
vm.obj.b = 2; // 속성 추가
delete vm.obj.a; // 속성 삭제
vm.arr[0] = 10; // 배열 인덱스 수정
vm.arr.length = 0; // 배열 길이 수정

// ✅ 특별한 메서드 사용 필요
Vue.set(vm.obj, 'b', 2);
Vue.delete(vm.obj, 'a');
vm.arr.splice(0, 1, 10);

// ===== Vue3 네이티브 지원 =====
const state = reactive({
obj: { a: 1 },
arr: [1, 2, 3]
});

// ✅ 다음 작업 모두 업데이트를 트리거
state.obj.b = 2; // 속성 추가
delete state.obj.a; // 속성 삭제
state.arr[0] = 10; // 배열 인덱스 수정
state.arr.length = 0; // 배열 길이 수정

요약​

Vue3가 Proxy를 사용하는 이유:

  1. ✅ 더 완전한 반응형 지원 (객체 속성 추가/삭제, 배열 인덱스 등)
  2. ✅ 성능 향상 (지연 처리, 미리 재귀할 필요 없음)
  3. ✅ 코드 간소화 (구현이 더 간결)
  4. ✅ 더 나은 개발 경험 (특별한 API를 기억할 필요 없음)

유일한 대가는 구 버전 브라우저(IE11) 지원을 포기한 것이지만, 이는 가치 있는 트레이드오프입니다.

Reference​