Vue | Vue3 - JS(ES6) Proxy 객체

Lumpen·2025년 11월 20일

Vue

목록 보기
4/12

Proxy는 ES6(ES2015)에 추가된 JavaScript 내장 객체로, 대상 객체(target)의 기본 동작을 가로채서 커스텀 동작을 정의할 수 있게 해줍니다.

Vue3의 반응성 시스템이 Proxy를 기반으로 동작하며, 이를 통해 자동 의존성 추적과 효율적인 UI 업데이트를 구현합니다.


기본 개념

Proxy란?

// 기본 문법
const proxy = new Proxy(target, handler);
  • target: 가로채고 싶은 원본 객체
  • handler: 가로채는 동작(트랩)을 정의하는 객체

기본 사용 예제

const target = { name: 'Kim', age: 25 };

const handler = {
  get(target, property) {
    console.log(`${property} 속성을 읽었습니다`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`${property} 속성을 ${value}로 변경합니다`);
    target[property] = value;
    return true; // set은 반드시 true/false 반환
  }
};

const proxy = new Proxy(target, handler);

// Proxy를 통해 접근
[proxy.name](http://proxy.name);        // "name 속성을 읽었습니다" → "Kim"
proxy.age = 30;    // "age 속성을 30로 변경합니다"

// 원본 객체는 변경됨
console.log(target.age); // 30

Proxy는 미들웨어 패턴

Proxy는 "구독"이 아니라 "가로채기(intercept)" 방식으로 동작합니다. 마치 Express 미들웨어나 네트워크 프록시 서버처럼, 모든 요청이 중간에서 가로채집니다.

사용자 코드
    ↓
proxy.count        // 접근 시도
    ↓
[Proxy Handler]    // ← 미들웨어!
    ↓
  get 트랩 실행
  - 로깅
  - 검증
  - 추적 등...
    ↓
target.count       // 실제 객체 접근
    ↓
값 반환 ← ← ← ← ← 역순으로 되돌아감

미들웨어 vs 구독 패턴

// ❌ 구독 패턴: 이벤트를 수동으로 발생시켜야 함
class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  subscribe(listener) {
    this.listeners.push(listener);
  }
  emit(event) {
    this.listeners.forEach(listener => listener(event));
  }
}

const emitter = new EventEmitter();
emitter.subscribe(data => console.log(data));
emitter.emit('change'); // 수동으로 이벤트 발생

// ✅ Proxy: 접근이 자동으로 가로채짐
const proxy = new Proxy({}, {
  set(target, property, value) {
    console.log('자동으로 감지!'); // 자동 실행
    target[property] = value;
    return true;
  }
});

[proxy.name](http://proxy.name) = 'Kim'; // set 트랩 자동 실행

트랩(Trap)의 종류

Proxy는 13가지 트랩을 제공합니다. 주요 트랩들:

const handler = {
  // 속성 읽기
  get(target, property, receiver) {
    return target[property];
  },
  
  // 속성 쓰기
  set(target, property, value, receiver) {
    target[property] = value;
    return true;
  },
  
  // 속성 존재 확인 (in 연산자)
  has(target, property) {
    return property in target;
  },
  
  // 속성 삭제 (delete 연산자)
  deleteProperty(target, property) {
    delete target[property];
    return true;
  },
  
  // 함수 호출
  apply(target, thisArg, argumentsList) {
    return target.apply(thisArg, argumentsList);
  },
  
  // new 연산자
  construct(target, argumentsList, newTarget) {
    return new target(...argumentsList);
  },
  
  // Object.keys(), for...in 등
  ownKeys(target) {
    return Object.keys(target);
  },
  
  // Object.getOwnPropertyDescriptor()
  getOwnPropertyDescriptor(target, property) {
    return Object.getOwnPropertyDescriptor(target, property);
  }
};

Vue의 Proxy 활용

reactive 구현 (단순화)

function reactive(target) {
  const handler = {
    get(target, property, receiver) {
      // 1. 의존성 추적
      track(target, property);
      
      // 2. 실제 값 반환
      const value = Reflect.get(target, property, receiver);
      
      // 3. 중첩된 객체도 reactive로 변환
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }
      
      return value;
    },
    
    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);
      
      // 값이 실제로 변경되었다면 UI 업데이트
      if (oldValue !== value) {
        trigger(target, property);
      }
      
      return result;
    }
  };
  
  return new Proxy(target, handler);
}

track과 trigger

let activeEffect = null;
const targetMap = new WeakMap();

// 의존성 추적
function track(target, property) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  let dep = depsMap.get(property);
  if (!dep) {
    depsMap.set(property, (dep = new Set()));
  }
  
  dep.add(activeEffect);
}

// 변경 알림
function trigger(target, property) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const dep = depsMap.get(property);
  if (!dep) return;
  
  dep.forEach(effect => effect());
}

Proxy + 구독 패턴 조합

Vue는 Proxy(미들웨어)track/trigger(구독 패턴)을 함께 사용합니다:

// 전체 동작 흐름
const state = reactive({ count: 0 });

// 1단계: 컴포넌트 렌더링
watchEffect(() => {
  console.log(state.count); // ← get 트랩 발동
});

/*
실행 과정:
1. state.count 접근
2. Proxy의 get 트랩 실행 (미들웨어)
3. track() 호출 → effect를 구독자로 등록 (구독 패턴)
4. 값 반환
*/

// 2단계: 값 변경
state.count = 5;

/*
실행 과정:
1. state.count 변경 시도
2. Proxy의 set 트랩 실행 (미들웨어)
3. 실제 값 변경
4. trigger() 호출 → 구독자들에게 알림 발송 (구독 패턴)
*/

핵심 차이:

  • Proxy: 접근을 가로채는 "센서" 역할 (미들웨어)
  • track: 누가 쓰는지 "기록"하는 구독 등록
  • trigger: 등록된 구독자에게 "알림" 발송

이 조합 덕분에 Vue는 자동으로 의존성을 추적하고 UI를 업데이트할 수 있습니다!


실전 활용 사례

1. 유효성 검증

function createValidator(schema) {
  return new Proxy({}, {
    set(target, property, value) {
      const validator = schema[property];
      
      if (validator && !validator(value)) {
        throw new Error(`Invalid value for ${property}`);
      }
      
      target[property] = value;
      return true;
    }
  });
}

const userSchema = {
  age: (value) => typeof value === 'number' && value > 0,
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};

const user = createValidator(userSchema);
user.age = 25;    // ✅ OK
user.age = -5;    // ❌ Error!

2. 읽기 전용 객체

function readonly(target) {
  return new Proxy(target, {
    set() {
      console.warn('This object is readonly!');
      return false;
    },
    deleteProperty() {
      console.warn('Cannot delete property!');
      return false;
    }
  });
}

const config = readonly({ apiUrl: 'https://api.example.com' });
config.apiUrl = 'other';  // 경고, 변경 안 됨

3. 기본값 제공

function withDefaults(target, defaults) {
  return new Proxy(target, {
    get(target, property) {
      return property in target ? target[property] : defaults[property];
    }
  });
}

const settings = withDefaults(
  { theme: 'dark' },
  { theme: 'light', lang: 'en', fontSize: 14 }
);

console.log(settings.theme);     // 'dark'
console.log(settings.lang);      // 'en'

4. 네거티브 인덱스 배열

function negativeArray(array) {
  return new Proxy(array, {
    get(target, property) {
      const index = Number(property);
      
      if (Number.isInteger(index) && index < 0) {
        return target[target.length + index];
      }
      
      return target[property];
    }
  });
}

const arr = negativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]);  // 5
console.log(arr[-2]);  // 4

Reflect API

Reflect는 Proxy의 트랩과 1:1 대응되는 메서드를 제공합니다.

Reflect를 사용하는 이유

const handler = {
  // ❌ 문제: getter의 this가 target을 가리킴
  get(target, property) {
    return target[property];
  },
  
  // ✅ 해결: this가 proxy를 가리킴
  get(target, property, receiver) {
    return Reflect.get(target, property, receiver);
  }
};

Reflect의 장점

  1. 일관성: Proxy 트랩과 1:1 대응
  2. 반환값: boolean으로 성공/실패 반환 (예외 없음)
  3. receiver: getter/setter의 올바른 this 바인딩
Reflect.get(target, property, receiver)
Reflect.set(target, property, value, receiver)
Reflect.has(target, property)
Reflect.deleteProperty(target, property)
Reflect.apply(func, thisArg, args)

한계와 주의사항

1. 원시값은 Proxy로 감쌀 수 없음

// ❌ 에러
const proxy = new Proxy(5, {});

// ✅ Vue의 ref가 필요한 이유
function ref(value) {
  return reactive({ value });
}

2. 성능 오버헤드

const obj = { x: 1 };
const proxy = new Proxy(obj, {
  get(t, p) { return t[p]; }
});

// 수백만 번 반복 시 성능 차이 발생
for (let i = 0; i < 1000000; i++) {
  obj.x;    // 빠름
  proxy.x;  // 조금 느림
}

3. 내장 객체 이슈

// Map, Set은 내부 슬롯 때문에 문제 발생
const map = new Map();
const proxiedMap = new Proxy(map, {});
proxiedMap.set('key', 'value'); // ❌ TypeError!

4. 동등성 비교

const obj = {};
const proxy1 = new Proxy(obj, {});
const proxy2 = new Proxy(obj, {});

console.log(proxy1 === proxy2); // false
console.log(proxy1 === obj);    // false

Vue3 vs Vue2

Vue2 (Object.defineProperty)

function observe(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    
    Object.defineProperty(obj, key, {
      get() { return value; },
      set(newValue) { value = newValue; }
    });
  });
}

// ❌ 한계
const data = observe({ count: 0 });
data.newProp = 10;  // 반응성 없음!
delete data.count;  // 반응성 없음!

Vue3 (Proxy)

function reactive(obj) {
  return new Proxy(obj, {
    get(target, property) {
      return target[property];
    },
    set(target, property, value) {
      target[property] = value;
      return true;
    },
    deleteProperty(target, property) {
      delete target[property];
      return true;
    }
  });
}

// ✅ 모든 동작이 반응형
const data = reactive({ count: 0 });
data.newProp = 10;  // ✅ 반응성!
delete data.count;  // ✅ 반응성!

정리

Proxy는 Vue3 반응성 시스템의 핵심입니다:

  1. 모든 접근 가로채기 → 의존성 자동 추적 (track)
  2. 모든 변경 가로채기 → 자동 UI 업데이트 (trigger)
  3. 동적 속성 추적 → Vue2의 한계 극복
  4. 깔끔한 코드$set, $delete 불필요

React는 명시적으로 setState를 호출해야 하지만, Vue는 Proxy가 자동으로 모든 것을 추적합니다. 이것이 Vue가 "마법 같다"고 느껴지는 이유입니다!

profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는

0개의 댓글