javascript proxy 객체의 힘: 프로처럼 연산을 가로채기

·5 min read·1·
JavaScript Proxy 객체의 힘: 프로처럼 연산을 가로채기

JavaScript Proxy 객체의 힘: 프로처럼 연산을 가로채기

🎯 개요

JavaScript는 유연성과 동적 특성으로 유명합니다. 하지만 때로는 단순히 데이터를 조작하는 것 이상이 필요할 때가 있습니다. 객체에서 수행되는 기본 연산들을 실제로 일어나기 전에 가로챌 수 있다면 어떨까요? 속성 접근, 할당, 함수 호출, 심지어 new 연산에도 사용자 정의 동작을 추가할 수 있다면요?

Proxy 객체를 만나보세요! ECMAScript 2015(ES6)에서 도입된 강력한 기능입니다. Proxy는 다른 객체(대상 객체)에 수행되는 연산을 가로채고 "트랩"할 수 있는 객체를 생성할 수 있게 해줍니다. 객체를 둘러싸는 보호하거나 향상시키는 레이어로 생각해보세요. 객체의 상호작용에 대한 완전한 제어권을 제공합니다.


🔍 Proxy란 무엇인가요?

Proxy 객체는 두 개의 인수로 생성됩니다:

  • target: 프록시로 감쌀 객체입니다. 함수, 배열, 심지어 다른 프록시도 될 수 있습니다.
  • handler: "트랩" 메서드들을 포함하는 객체입니다. 이 메서드들은 가로채고 싶은 연산에 대한 사용자 정의 동작을 정의합니다.
const target = {}; // 원본 객체
const handler = {}; // 트랩 메서드를 가진 객체
const proxy = new Proxy(target, handler);


🛠️ 주요 트랩(Traps) 이해하기

1. get 트랩 - 속성 읽기 가로채기

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

const personProxy = new Proxy(person, {
  get(target, property) {
    console.log(`속성 ${property}에 접근 중...`);
    return target[property];
  }
});

console.log(personProxy.name);
// 출력: 속성 name에 접근 중...
// 출력: Alice

2. set 트랩 - 속성 할당 가로채기

const validatedUser = new Proxy({}, {
  set(target, property, value) {
    if (property === 'age' && typeof value !== 'number') {
      throw new TypeError('나이는 숫자여야 합니다!');
    }

    if (property === 'age' && value < 0) {
      throw new RangeError('나이는 음수일 수 없습니다!');
    }

    target[property] = value;
    return true; // 할당 성공을 나타냄
  }
});

validatedUser.name = "Bob"; // ✅ 성공
validatedUser.age = 25; // ✅ 성공
validatedUser.age = -5; // ❌ RangeError 발생

3. has 트랩 - in 연산자 가로채기

const hiddenProps = new Proxy({ visible: true, _secret: 'hidden' }, {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // 비밀 속성은 숨김
    }
    return property in target;
  }
});

console.log('visible' in hiddenProps); // true
console.log('_secret' in hiddenProps); // false (실제로는 존재함)


🎨 실용적인 사용 사례

🔐 1. 데이터 보안 및 접근 제어

const secureData = new Proxy({
  publicInfo: '모든 사용자가 접근 가능',
  confidential: '기밀 정보'
}, {
  get(target, property) {
    if (property === 'confidential') {
      throw new Error('인증되지 않은 접근입니다!');
    }
    return target[property];
  }
});

console.log(secureData.publicInfo); // ✅ 접근 허용
console.log(secureData.confidential); // ❌ Error 발생

📝 2. 기본값 제공

const defaultValues = {
  name: "익명",
  age: 0,
  country: "미국"
};

const userWithDefaults = new Proxy({}, {
  get(target, property) {
    return property in target ? target[property] : defaultValues[property];
  }
});

userWithDefaults.name = "Charlie";
console.log(userWithDefaults.name); // Charlie
console.log(userWithDefaults.age); // 0 (기본값)
console.log(userWithDefaults.country); // 미국 (기본값)

🚀 3. 메모이제이션 (성능 최적화)

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = new Proxy({}, {
  get(target, property) {
    const n = Number(property);
    if (!(property in target)) {
      console.log(`피보나치(${n}) 계산 중...`);
      target[property] = fibonacci(n);
    } else {
      console.log(`피보나치(${n}) 캐시에서 반환`);
    }
    return target[property];
  }
});

console.log(memoizedFib[10]); // 계산됨
console.log(memoizedFib[10]); // 캐시에서 반환

🔍 4. 디버깅 및 로깅

function createLogger(obj, name) {
  return new Proxy(obj, {
    get(target, property) {
      console.log(`🔍 [${name}] 속성 '${property}' 읽기: ${target[property]}`);
      return target[property];
    },
    set(target, property, value) {
      console.log(`✏️ [${name}] 속성 '${property}' 설정: ${value}`);
      target[property] = value;
      return true;
    }
  });
}

const loggedUser = createLogger({ name: "Dave" }, "User");
loggedUser.age = 28; // ✏️ [User] 속성 'age' 설정: 28
console.log(loggedUser.name); // 🔍 [User] 속성 'name' 읽기: Dave


⚡ 고급 기능

🔄 Revocable Proxy (철회 가능한 프록시)

때로는 프록시를 나중에 비활성화해야 할 수도 있습니다:

const target = { message: "안녕하세요!" };
const { proxy, revoke } = Proxy.revocable(target, {
  get(target, property) {
    return `가로채짐: ${target[property]}`;
  }
});

console.log(proxy.message); // "가로채짐: 안녕하세요!"

revoke(); // 프록시 비활성화

console.log(proxy.message); // ❌ TypeError: 철회된 프록시에서 'get' 수행 불가

🪞 Reflect API와 함께 사용하기

Reflect API는 Proxy와 완벽하게 호환됩니다:

const smartProxy = new Proxy(target, {
  get(target, property, receiver) {
    console.log(`${property} 속성에 접근`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`${property} 속성을 ${value}로 설정`);
    return Reflect.set(target, property, value, receiver);
  }
});


⚠️ 주의사항 및 제한사항

성능 고려사항

  • Proxy는 추가적인 레이어를 도입하므로 성능에 약간의 영향을 줄 수 있습니다
  • 크리티컬한 성능이 요구되는 곳에서는 신중하게 사용하세요

브라우저 호환성

  • IE에서는 지원되지 않습니다 (폴리필 불가)
  • 최신 브라우저에서는 완전히 지원됩니다

투명성 제한

  • === 엄격한 등호 비교는 가로챌 수 없습니다
  • 내장 객체의 내부 슬롯은 프록시할 수 없습니다

🎯 언제 Proxy를 사용해야 할까요?

다음과 같은 경우에 사용하세요:

  • 객체 상호작용에 대한 사용자 정의 제어가 필요할 때
  • 로깅 및 디버깅 목적
  • 유효성 검사 규칙 적용 및 불변성 강제
  • 동적 데이터 구조 구축 (예: 음수 배열 인덱싱)
  • 함수의 동작 확장 및 래핑

다음과 같은 경우는 피하세요:

  • 단순한 객체 조작으로 충분할 때
  • 성능이 매우 중요한 애플리케이션
  • 코드의 복잡성을 불필요하게 증가시키는 경우

🎉 결론

JavaScript의 Proxy 객체는 객체 동작을 커스터마이징할 수 있는 다재다능하고 강력한 도구입니다. 로깅과 유효성 검사부터 동적 속성 생성과 특수한 경우 처리까지, Proxy는 JavaScript 애플리케이션의 기능을 크게 향상시킬 수 있는 유연한 방법을 제공합니다.

다양한 트랩과 그 적용 방법을 이해함으로써, 더욱 정교하고 제어 가능한 JavaScript 애플리케이션을 구축할 수 있습니다. Proxy는 추가적인 제어 레이어를 더하지만, 과도하게 사용하면 복잡성을 증가시킬 수 있으므로 신중하게 사용해야 합니다.


💡 팁: Proxy는 메타프로그래밍의 강력한 도구입니다. 프레임워크나 라이브러리를 만들 때, 또는 객체의 동작에 대한 세밀한 제어가 필요할 때 특히 유용합니다!


📚 출처