Chapter 14. 메타프로그래밍

허상범·2023년 3월 6일
0
post-thumbnail

1. 메타프로그래밍
2. 자바스크립트 프록시 패턴
3. Proxy & Reflect
4. Proxy.revocable
5. 활용 - 모든 프로퍼티 은닉화
6. 활용 - 한시적 접근 허용


1. 메타프로그래밍

메타프로그래밍(Metaprogramming)이란 자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성·수정하는 것을 말한다. 넓은 의미에서, 런타임에 수행해야 할 작업의 일부를 컴파일 타임 동안 수행하는 프로그램을 말하기도 한다.
https://ko.wikipedia.org/wiki/%EB%A9%94%ED%83%80%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D

  • 일반적인 프로그래밍이 데이터를 조작하는 코드를 작성한다면 메타프로그래밍은 다른 코드를 조작하는 코드를 작성한다.
  • 객체를 다루는 메소드들은 넓은 의미에서 메타프로그래밍이다.
  • ex. getter, setter 등

2. 자바스크립트 프록시 패턴

  • 대상 객체에 대한 인터렉션(접근)을 가로채서 제어한다.
  • 프록시 패턴을 사용하면 대상 객체 대신에 프록시 객체가 대상 객체에 접근하는 요청을 가로채서 제어한다.

3. Proxy & Reflect

const proxy = new Proxy(target, handler);
  • target : 감싸게 될 대상 객체
  • handler : 동작을 가로채는 메서드인 trap이 담긴 객체로, 여기서 프록시를 설정
  • proxy에 작업이 가해지고, handler에 작업과 상응하는 trap이 있으면 trap이 실행
    • trap이 없으면 target에 작업이 직접 수행

trap이 없는 프록시

  • trap이 없으면 target에 작업이 직접 수행
  • handler가 비어있으면 Proxy에 가해지는 작업은 곧바로 target에 전달
const name = {
  firstName: 'Sangbeom',
  lastName: 'Heo',
};

const proxy = new Proxy(name, {}); // no traps

proxy.firstName = 'first';
proxy.lastName = 'last';

console.log(name); // {firstName: "first", lastName: "last"}

trap이 있는 프록시와 Reflect

  • proxy가 오버라이딩할 수 있는 기본 동작들은 Reflect에 정의되어 있으며, Proxy의 trapReflect의 메소드는 1:1로 매칭된다.
  • Reflect : 기본동작들을 똑같이 반영해서 정의해 놓음. 프록시가 가로채서 로직을 진행한 후 기본동작을 호출하고 싶을 때 사용한다.

Proxy Trap기본 동작to override
getReflect.get()getter, 프로퍼티를 읽을 때
setReflect.set()setter, 프로퍼티에 쓸 때
hasReflect.has()in 연산자가 동작할 때
deletePropertyReflect.deleteProperty()delete 연산자가 동작할 때
getPrototypeOfReflect.getPrototypeOf()Object.getPrototypeOf()
setPrototypeOfReflect.setPrototypeOf()Object.setPrototypeOf()
isExtensibleReflect.isExtensible()Object.isExtensible()
preventExtensionsReflect.preventExtensions()Object.preventExtensions()
getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor()
definePropertyReflect.defineProperty()Object.defineProperty()
ownKeysReflect.ownKeys()Object.getOwnPropertyNames(),
Object.getOwnPropertySymbols(),
Object.keys()
applyReflect.apply()함수를 호출할 때
constructReflect.construct()new 연산자가 동작할 때
const person = {
  firstName: 'Sangbeom',
  lastName: 'Heo',
  age: 20,
};

const personProxy = new Proxy(person, {
  set(target, key, value, receiver) {
    if (!target.hasOwnProperty(key)) {
      console.log('객체에 존재하지 않는 프로퍼티');
      return;
    }
    if (key === 'age' && typeof value !== 'number') {
      console.log('age의 값은 number 타입만 올 수 있다.');
    }
    return Reflect.set(target, key, value, receiver);
  },
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});

personProxy.age = 10;
personProxy.number = 20; // 객체에 존재하지 않는 프로퍼티
personProxy.age = '10'; // age의 값은 number 타입만 올 수 있다.

4. Proxy.revocable

Proxy.revocable(target, handler);
  • Static Method
  • 나중에 취소가 가능한 프록시 객체를 만든다.
  • 취소 후에는 다시 일반 객체처럼 동작한다.
const person = {
  name: 'heo',
  age: 10,
};

const revocableProxy = Proxy.revocable(person, {
  deleteProperty(target, key) {
    return key === 'age' ? false : Reflect.deleteProperty(target, key);
    // age 일 때 삭제를 못하게 막음
  },
});

console.log(revocableProxy.proxy);
delete revocableProxy.proxy.name;
console.log(revocableProxy.proxy);
delete revocableProxy.proxy.age; // 삭제 안됌
console.log(revocableProxy.proxy);

5. 활용 - 모든 프로퍼티 은닉화

const ProxyClass = (() => {
  const PROP = Symbol('PROP');
  const handler = {
    set(target, key, value) {
      target[PROP].set(key, value);
    },
    get(target, key) {
      return target[PROP].get(key);
    },
  };
  return class {
    constructor() {
      this[PROP] = new Map();
      return new Proxy(this, handler);
    }
  };
})();

const instance = new ProxyClass();
instance.a = 10;
instance.hasOwnProperty('a'); // TypeError: instance.hasOwnProperty is not a function
// 프로퍼티에 직접 접근할 수 없다.

6. 활용 - 한시적 접근 허용

const getPostInfo = postId =>
  fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    .then(res => res.json())
    .then(res => {
      let counter = 0;
      const pr = Proxy.revocable(res, {
        get(target, key) {
          if (counter++ > 2) {
            //2번 동작 후 프록시 종료
            pr.revoke();
          }
          return res[key];
        },
      });
      return pr.proxy;
    })
    .catch(err => {
      console.error(err);
    });

getPostInfo(1).then(post => {
  console.log(`id: ${post.id}`);
  console.log(`title: ${post.title}`);
  console.log(`body: ${post.body}`); // TypeError: Cannot perform 'get' on a proxy that has been revoked
});


참고자료

0개의 댓글