Ray Book
JavaScript Deep Dive

Proxy와 Reflect

객체 위에 가상 레이어를 씌워 모든 작업을 가로채는 Proxy, get, set, has 등 핵심 트랩과 Reflect의 관계를 시각화합니다

javascriptproxyreflectmetaprogramming

Proxy란

Proxy는 ES2015에서 도입된 메타프로그래밍 기능입니다. 대상 객체(target) 위에 가상 레이어 를 씌워, 속성 접근, 할당, 열거 등 거의 모든 기본 작업을 가로챌 수 있습니다.

const proxy = new Proxy(target, handler);
  • target, 감쌀 원본 객체 (어떤 객체든 가능)
  • handler, 가로챌 작업을 정의하는 객체. handler의 메서드를 트랩(trap) 이라 합니다

handler가 빈 객체 {}이면 모든 작업이 target에 그대로 전달됩니다. 트랩을 정의하면 해당 작업만 가로채고, 나머지는 원래대로 동작합니다.

핵심 트랩

ECMAScript 스펙에서 정의하는 Proxy 트랩은 13가지입니다. 가장 자주 쓰이는 트랩을 먼저 살펴보겠습니다.

get / set

속성 읽기쓰기 를 가로챕니다.

const handler = {
  get(target, prop, receiver) {
    console.log(`읽기: ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`쓰기: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },
};

const obj = new Proxy({ name: "Ray" }, handler);
obj.name;       // 읽기: name → "Ray"
obj.age = 25;   // 쓰기: age = 25
코드
1const handler = {
2 get(target, prop, receiver) {
3 console.log(`GET ${prop}`);
4 return Reflect.get(target, prop, receiver);
5 },
6 set(target, prop, value, receiver) {
7 console.log(`SET ${prop} = ${value}`);
8 return Reflect.set(target, prop, value, receiver);
9 },
10};
11 
12const target = { name: "Ray", age: 25 };
13const proxy = new Proxy(target, handler);
14 
15proxy.name; // GET name → "Ray"
16proxy.age = 26; // SET age = 26 → true
17proxy.job = "dev"; // SET job = dev → true
Target 객체
name"Ray"
age25
Handler Traps
get()set()
인터셉트
Proxy 생성
target과 handler가 연결됨
target 객체와 handler를 전달해 Proxy를 생성합니다. handler의 메서드(trap)가 target에 대한 작업을 가로챕니다.

has / deleteProperty

in 연산자와 delete 연산자를 가로챕니다.

const handler = {
  has(target, prop) {
    // _로 시작하는 속성은 숨김
    if (prop.startsWith("_")) return false;
    return Reflect.has(target, prop);
  },
  deleteProperty(target, prop) {
    if (prop.startsWith("_")) {
      throw new Error("비공개 속성은 삭제할 수 없습니다");
    }
    return Reflect.deleteProperty(target, prop);
  },
};

const obj = new Proxy({ _secret: 42, name: "Ray" }, handler);
"name" in obj;    // true
"_secret" in obj; // false (숨겨짐)

apply / construct

함수 호출과 new 연산자를 가로챕니다. target이 함수 일 때만 사용할 수 있습니다.

function sum(a, b) {
  return a + b;
}

const proxiedSum = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`호출: sum(${args.join(", ")})`);
    return Reflect.apply(target, thisArg, args);
  },
});

proxiedSum(1, 2); // 호출: sum(1, 2) → 3

전체 트랩 목록

트랩가로채는 작업
get속성 읽기
set속성 쓰기
hasin 연산자
deletePropertydelete 연산자
apply함수 호출
constructnew 연산자
getPrototypeOfObject.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()
ownKeysObject.keys(), for...in

Reflect

Reflect는 Proxy 트랩과 1:1로 대응하는 13개의 정적 메서드를 가진 내장 객체입니다. 각 메서드는 해당 작업의 기본 동작 을 수행합니다.

// Reflect 없이, 기본 동작을 직접 구현
const handler = {
  get(target, prop) {
    return target[prop]; // 단순하지만 receiver를 무시
  },
};

// Reflect 사용, 기본 동작을 정확하게 위임
const handler2 = {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver); // receiver 전달
  },
};

Reflect를 사용하는 이유:

  1. 정확한 기본 동작 , target[prop] 대신 Reflect.get(target, prop, receiver)를 사용하면 getter의 this가 올바르게 설정됩니다
  2. 일관된 반환값 , Reflect.set()은 성공 여부를 boolean으로 반환합니다. strict mode에서 set 트랩은 반드시 boolean을 반환해야 합니다
  3. 명확한 의도 , "기본 동작을 수행한다"는 의도가 코드에 드러납니다

실용적 활용

유효성 검사

Proxy의 가장 대표적인 활용입니다. 객체에 투명한 검증 계층 을 추가합니다.

function createValidated(target, validators) {
  return new Proxy(target, {
    set(target, prop, value) {
      const validate = validators[prop];
      if (validate && !validate(value)) {
        throw new TypeError(
          `"${prop}"에 대한 유효성 검사 실패: ${value}`
        );
      }
      return Reflect.set(target, prop, value);
    },
  });
}

const person = createValidated(
  { age: 25, name: "Ray" },
  {
    age: (v) => typeof v === "number" && v >= 0,
    name: (v) => typeof v === "string" && v.length > 0,
  }
);

person.age = 30;    // OK
person.age = -1;    // TypeError: "age"에 대한 유효성 검사 실패: -1
person.name = "";   // TypeError: "name"에 대한 유효성 검사 실패:
코드
1const handler = {
2 set(target, prop, value) {
3 if (prop === "age") {
4 if (typeof value !== "number" || value < 0) {
5 throw new TypeError("age must be a non-negative number");
6 }
7 }
8 return Reflect.set(target, prop, value);
9 },
10};
11 
12const person = new Proxy({ age: 25 }, handler);
13 
14person.age = 30; // OK
15person.age = -1; // TypeError!
16person.name = "A"; // OK (age 아님)
Target 객체
age25
Handler Traps
set()
인터셉트
Proxy 생성
유효성 검사 Proxy 생성됨
set 트랩에서 age 속성에 대한 유효성 검사를 수행하는 Proxy를 생성합니다.

로깅 / 디버깅

모든 속성 접근을 자동으로 기록합니다.

function withLogging(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      console.log(`[GET] ${String(prop)}`);
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      console.log(`[SET] ${String(prop)} = ${value}`);
      return Reflect.set(target, prop, value, receiver);
    },
  });
}

반응형 시스템 (Vue 3)

Vue 3의 반응형 시스템은 Proxy를 기반으로 합니다. reactive()가 반환하는 객체는 Proxy입니다.

// Vue 3 반응형의 핵심 원리 (간략화)
function reactive(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      track(target, prop);          // 의존성 추적
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      trigger(target, prop);        // 변경 알림
      return result;
    },
  });
}

get 트랩에서 "이 속성을 누가 읽었는지" 기록하고, set 트랩에서 "이 속성이 바뀌었으니 다시 렌더링하라" 알립니다. Vue 2에서 사용하던 Object.defineProperty는 새 속성 추가를 감지하지 못했지만, Proxy는 모든 작업을 가로채므로 이 한계가 해결됩니다.

MobX도 observable() 함수에서 Proxy를 사용하여 객체의 속성 변경을 자동으로 감지하고 관련 reaction을 실행합니다. 이처럼 Proxy 기반 반응형 시스템은 현대 프론트엔드 상태 관리의 핵심 패턴이 되었습니다.

Revocable Proxy

Proxy.revocable()취소 가능한 Proxy 를 생성합니다. revoke()를 호출하면 Proxy가 비활성화되어 이후 모든 작업이 TypeError를 발생시킵니다.

const { proxy, revoke } = Proxy.revocable(
  { data: "민감한 정보" },
  {
    get(target, prop, receiver) {
      return Reflect.get(target, prop, receiver);
    },
  }
);

proxy.data; // "민감한 정보"

revoke(); // Proxy 비활성화

proxy.data; // TypeError: Cannot perform 'get' on a proxy
            // that has been revoked

임시 접근 권한을 부여하거나, 특정 시점 이후 객체 접근을 차단할 때 유용합니다.

성능 고려사항

Proxy는 강력하지만, 모든 작업에 트랩 함수 호출이 추가 됩니다. 고려할 점:

  1. 핫 패스 주의 , 초당 수백만 번 접근하는 속성에 Proxy를 쓰면 성능에 영향이 있습니다
  2. 트랩 최소화 , 필요한 트랩만 정의하세요. 정의하지 않은 트랩은 target으로 직접 전달됩니다
  3. 중첩 Proxy 주의 , Proxy 위에 Proxy를 겹치면 트랩 호출이 누적됩니다
  4. V8 최적화 제한 , Proxy 객체는 hidden class 기반의 인라인 캐싱 최적화를 받기 어렵습니다

일반적인 애플리케이션 코드에서는 성능 차이가 체감되지 않지만, 라이브러리 내부의 핫 패스에서는 벤치마크를 통해 확인하는 것이 좋습니다.

정리

개념역할
Proxy객체 위에 가상 레이어를 씌워 작업을 가로챔
Handler트랩 메서드를 정의하는 객체
Trap특정 작업을 가로채는 handler의 메서드 (13종)
Reflect각 트랩의 기본 동작을 수행하는 정적 메서드 (13종)
Revocable취소 가능한 Proxy. revoke()로 비활성화

다음 단계

지금까지 JavaScript의 고급 기능들을 살펴봤습니다. 다음 마지막 글에서는 실무에서 자주 혼동하는 패턴들 을 정리하고, 시리즈를 마무리합니다.