[JavaScript] Proxy를 활용한 객체 동작 제어

Proxy 기본 문법

JavaScript Proxy는 원본 객체에 대한 중개자 또는 래퍼 역할을 하는 특수 객체로, 대상 객체라고 합니다.

Proxy는 두 개의 인수를 받습니다.

  • target: 실제 동작을 수행할 원본 객체 (함수도 가능합니다).
  • handler: target에 가해지는 작업을 가로채는 트랩 메서드들을 포함하는 객체입니다.
const proxy = new Proxy(target, handler);

handler 빈 객체 제공 Proxy

빈 hanlder를 통해서 생성되었으므로 proxy를 통해 수행된 작업은 기본 동작을 변경하지 않고 target 객체에 직접 전달됩니다.

const target = { message: "Hello, Proxy!" };
const proxy = new Proxy(target, {}); // 핸들러가 빈 객체인 경우

// 트랩이 없으므로 모든 작업이 target에 위임됩니다.
console.log(proxy.message); // "Hello, Proxy!"

proxy.newProp = 123;
console.log(target.newProp); // 123

Proxy get 트랩

프록시의 get 트랩을 사용하면, target에 없는 프로퍼티에 접근할 때 기본값을 반환하거나 추가 로직을 수행할 수 있습니다.

const person = { name: "Alice", age: 30 };

const personProxy = new Proxy(person, {
  get(target, prop) {
    // target에 prop이 있으면 값을 반환하고, 없으면 기본값을 반환합니다.
    return prop in target ? target[prop] : "N/A";
  }
});

console.log(personProxy.name); // "Alice"
console.log(personProxy.email); // "N/A"

프로퍼티가 target 객체에 있으면 트랩은 프로퍼티 값을 반환하고, 프로퍼티가 없으면 트랩은 "N/A"의 기본값을 반환합니다.

Proxy set 트랩

set 트랩을 통해 객체에 프로퍼티를 설정할 때, 입력 값의 타입이나 조건을 검증할 수 있습니다.

let numbers = [];

numbers = new Proxy(numbers, {
  set(target, prop, value) {
    if (typeof value !== "number") {
      console.error(`숫자만 허용됩니다. '${value}'는 유효하지 않습니다.`);
      return false; // falsy 값을 반환하면 TypeError가 발생합니다.
    }
    // 값을 정상적으로 저장한 후 true를 반환합니다.
    target[prop] = value;
    return true;
  }
});

numbers.push(10);    // 정상적으로 작동합니다.
numbers.push("test"); // 콘솔에 오류 메시지를 출력하고 값이 저장되지 않습니다.

console.log(numbers); // [10]

set() 트랩은 numbers Proxy 객체의 프로퍼티에 값을 할당할 때 가로채도록 정의됩니다. 트랩은 할당되는 값이 숫자인지 확인하고, 숫자가 아니면 오류 메시지를 콘솔에 기록하고 false를 반환하여 할당에 실패했음을 나타냅니다. set() 트랩은 데이터 유효성 검사를 적용하고 객체가 특정 데이터 유형 제약 조건을 준수하는지 확인하는 데 유용합니다.

Proxy ownKeys 트랩

객체 순환 작업(예: Object.keys, for...in)은 내부 메서드 [[OwnPropertyKeys]]를 사용합니다. ownKeys 트랩을 구현하면, 특정 조건에 따라 순회 대상에서 일부 프로퍼티를 제외할 수 있습니다.

const data = {
  publicInfo: "Visible",
  _secret: "Hidden"
};

const secureData = new Proxy(data, {
  ownKeys(target) {
    // 밑줄로 시작하지 않는 키만 반환합니다.
    return Reflect.ownKeys(target).filter(key => typeof key === "string" && !key.startsWith('_'));
  }
});

console.log(Object.keys(secureData)); // ["publicInfo"]
for (const key in secureData) {
  console.log(key); // "publicInfo"만 순회됩니다.
}

위의 예에서 ownKeys() 트랩은 secureData Proxy 객체의 순회 작업을 가로채도록 정의됩니다. 트랩은 밑줄(_)로 시작하지 않는 키만 반환하여 이름이 밑줄로 시작하는 프로퍼티를 순회에서 제외합니다. ownKeys() 트랩은 특정 프로퍼티를 숨기거나 특정 기준에 따라 객체의 순회 동작을 사용자 정의하는 데 유용합니다.

Proxy apply 트랩

함수도 객체이므로 Proxy로 감쌀 수 있습니다. apply 트랩은 프록시를 함수처럼 호출할 때 실행됩니다. apply() 트랩을 사용하면 함수 호출에 대한 사용자 정의 로직을 구현하고, 함수 실행 전에 사전 처리 또는 사후 처리 작업을 수행하거나, 함수 호출 자체를 완전히 대체할 수 있습니다.

function greet(name) {
  console.log(`Hello, ${name}!`);
}

const delayedGreet = new Proxy(greet, {
  apply(target, thisArg, args) {
    // 2초 지연 후 함수를 호출합니다.
    setTimeout(() => {
      target.apply(thisArg, args);
    }, 2000);
  }
});

console.log(delayedGreet.length); // 원본 함수의 length 값을 출력합니다(예: 1).
delayedGreet("Bob"); // 2초 후 "Hello, Bob!"을 출력합니다.

프록시 해제하기

경우에 따라 Proxy를 사용하다가 더 이상 Proxy 기능을 사용하지 않고 원본 객체에 접근하고 싶을 때, revocable Proxy를 사용할 수 있습니다. revocable Proxy는 생성 시 함께 반환되는 revoke 함수를 호출하면 Proxy를 해제합니다.

const config = { setting: "on" };

const { proxy: configProxy, revoke } = Proxy.revocable(config, {});

console.log(configProxy.setting); // "on"

// 나중에 더 이상 Proxy를 사용하지 않으려면 revoke() 호출
revoke();

try {
  console.log(configProxy.setting); // Error: Proxy has been revoked
} catch (err) {
  console.error("프로시 해제 후 접근 에러:", err.message);
}

Reflect API

내부 메서드(예: [[Get]], [[Set]], [[Delete]] 등)는 명세서에서만 정의된 메서드로 직접 호출할 수 없지만, Reflect 객체는 이들 내부 동작을 함수 형태로 제공하여 Proxy 트랩 내에서 쉽게 활용할 수 있습니다.

const targetObj = { username: "alice" };

const proxyObj = new Proxy(targetObj, {
  get(target, prop, receiver) {
    console.log(`프로퍼티 "${prop}"에 접근합니다.`);
    // Reflect.get을 사용하면 receiver(일반적으로 프록시)를 올바른 `this`로 전달할 수 있습니다.
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`프로퍼티 "${prop}"에 값 ${value}를 설정합니다.`);
    // Reflect.set은 작업 성공 시 true를 반환합니다.
    return Reflect.set(target, prop, value, receiver);
  }
});

console.log(proxyObj.username); // "프로퍼티 "username"에 접근합니다." 뒤에 "alice" 출력
proxyObj.username = "bob";      // "프로퍼티 "username"에 값 bob을 설정합니다." 출력
console.log(targetObj.username); // "bob"
  • Reflect 메서드들은 예외를 던지지 않고 성공/실패를 나타내는 불리언 값을 반환합니다.
  • Reflect 메서드들은 프록시 트랩과 정확히 같은 이름과 매개변수를 가집니다.
  • Reflect를 사용하면 원래 객체의 기본 동작을 쉽게 유지할 수 있습니다.

'JavaScript' 카테고리의 다른 글

[JavaScript] 함수 커링  (0) 2025.02.19
[JavaScript] 내장 함수 eval  (0) 2025.02.19
[JavaScript] 동적 모듈 가져오기  (0) 2025.02.19
[JavaScript] 모듈 내보내기 및 가져오기  (0) 2025.02.19
[JavaScript] 모듈 시스템  (0) 2025.02.19