Proxy, Reflect와 메타프로그래밍
ES6에서는 Proxy, Reflect 객체를 통해 메타프로그래밍을 할 수 있습니다. 메타프로그래밍이 무엇인지? 그리고 Proxy와 Reflect에 대해서 알아보도록 하겠습니다.
Metaprogramming
메타프로그래밍은 프로그램이 자기 자신을 조작할 수 있는 능력을 말합니다. 이를 통해 프로그램은 동적으로 자기 자신을 변화시키거나, 다른 프로그램을 분석하거나, 새로운 코드를 생성할 수 있습니다. 메타프로그래밍을 통해 코드의 유연성과 재사용성을 높일 수 있으며, 런타임에서 객체의 동작을 수정할 수 있어서 더욱 강력한 코드를 작성할 수 있습니다.
메타프로그래밍에서 가장 중요한 세 가지 개념
-
Introspection
Introspection은 코드가 자기 자신을 검사할 수 있는 능력을 의미합니다. 이를 통해 코드의 내부 구조와 데이터를 검사하거나, 코드의 실행 시점에서 객체의 메소드나 프로퍼티에 접근할 수 있습니다. 자바스크립트에서는 ES6부터 Reflect 객체를 제공하여 Introspection을 지원하고 있습니다.
-
ES6 이전(
Object.getOwnPropertyDescriptor
)const myObject = { myProperty: 'Hello, World!' }; // myObject의 myProperty 프로퍼티 값에 접근하기 console.log(Object.getOwnPropertyDescriptor(myObject, 'myProperty').value); // 'Hello, World!' // myObject의 프로퍼티를 추가하기 Object.defineProperty(myObject, 'myNewProperty', { value: 'Goodbye, World!', writable: true, configurable: true, enumerable: true }); console.log(myObject.myNewProperty); // 'Goodbye, World!' // myObject의 프로퍼티를 제거하기 delete myObject.myNewProperty; console.log(myObject); // { myProperty: 'Hello, World!' }
-
ES6 이후(
Reflect
)const myObject = { myProperty: 'Hello, World!' }; // myObject의 myProperty 프로퍼티 값에 접근하기 console.log(Reflect.get(myObject, 'myProperty')); // 'Hello, World!' // myObject의 프로퍼티를 추가하기 Reflect.set(myObject, 'myNewProperty', 'Goodbye, World!'); console.log(myObject.myNewProperty); // 'Goodbye, World!' // myObject의 프로퍼티를 제거하기 Reflect.deleteProperty(myObject, 'myNewProperty'); console.log(myObject); // { myProperty: 'Hello, World!' }
-
-
Self-Modification
Self-Modification은 코드가 자기 자신을 수정할 수 있는 능력을 의미합니다. 이를 통해 코드를 동적으로 생성하거나, 객체의 동작을 수정할 수 있습니다. 자바스크립트에서는 이러한 Self-Modification이 보안상 문제가 될 수 있으므로, 보안성을 고려한 코드 작성이 필요합니다.
- 보안상의 이슈로 인해
eval()
,Function 생성자
함수의 사용은 권장되지 않으며, 다른 방법을 사용 권장
// eval let x = 1; eval('x = 2'); console.log(x); // 출력값: 2 // Function 생성자 var myFunction = new Function("a", "b", "return a * b"); console.log(myFunction(3, 4)); // 출력: 12
- 보안상의 이슈로 인해
-
Intercession: “대신 해 주기”를 의미. 메타프로그래밍에서 Intercession은 wrapping, trapping, intercepting과 같은 기법을 사용하여 다른 객체나 코드의 동작을 대신해서 제어하거나 수정할 수 있습니다.
-
ES6 이전 (
Object.defineProperty()
,Object get(), set()
)// Object.defineProperty() 사용 var sun = {}; Object.defineProperty(sun, 'rises', { value: true, configurable: false, writable: false, enumerable: false }); console.log('sun rises', sun.rises); sun.rises = false; console.log('sun rises', sun.rises); // 객체의 get, set 메소드 사용 const myObj = { name: 'Alice', age: 20, get: function(prop) { console.log(`getting ${prop}`); return this[prop]; }, set: function(prop, value) { console.log(`setting ${prop} to ${value}`); this[prop] = value; } }; myObj.get('name'); // "getting name" // "Alice" myObj.set('age', 30); // "setting age to 30" console.log(myObj.age); // 30
-
ES6 이후 (Proxy)
const myObj = { name: 'Alice', age: 20 }; const handler = { get: function(target, prop) { console.log(`getting ${prop}`); return target[prop]; }, set: function(target, prop, value) { console.log(`setting ${prop} to ${value}`); target[prop] = value; } }; const proxyObj = new Proxy(myObj, handler); proxyObj.name; // "getting name" -> "Alice" proxyObj.age = 30; // "setting age to 30" console.log(proxyObj.age); // 30
-
Proxy
Proxy 라는 단어는 대리인, 대행자, 대리인 역할을 하는 사람 또는 것을 의미합니다. 컴퓨터 과학에서 Proxy 는 다른 객체에 대한 접근을 제어하거나 중개하는 데 사용되는 객체를 가리킵니다.
let proxy = new Proxy(target, handler);
ES6에서는 Proxy 객체를 통해 메타프로그래밍을 할 수 있습니다. Proxy는 대상 객체를 감싸고 대상 객체의 동작을 가로채서 다양한 작업을 수행할 수 있습니다. 예를 들어, 객체의 프로퍼티에 접근하거나 값을 변경하기 전에 검증을 수행할 수 있습니다.
API
handler.apply()
handler.construct()
handler.get()
handler.has()
handler.ownKeys()
handler.set()
handler.setPrototypeOf()
handler.getPrototypeOf()
handler.defineProperty()
handler.deleteProperty()
handler.getOwnPropertyDescriptor()
handler.preventExtensions()
handler.isExtensible()
예시
const target = {
name: "John",
age: 30
};
const proxy = new Proxy(target, {
get(target, key) {
console.log(`Accessing ${key} property`);
return target[key];
},
set(target, key, value) {
console.log(`Setting ${key} property to ${value}`);
target[key] = value;
}
});
console.log(proxy.name); // "Accessing name property" 출력 후 "John" 반환
proxy.age = 35; // "Setting age property to 35" 출력
위 코드에서는 Proxy 객체를 생성하여 **get
**과 set
메소드를 정의하였습니다. get
메소드는 프로퍼티에 접근할 때 호출되며, set
메소드는 프로퍼티 값을 변경할 때 호출됩니다. 이를 이용하여 프로퍼티에 접근하거나 값을 변경할 때 로그를 출력하는 작업을 추가하였습니다.
Reflect
ES6에서는 Reflect 객체를 통해 Introspection을 할 수 있습니다. Reflect는 자바스크립트의 내장 객체로, 객체와 관련된 메소드를 제공합니다. 예를 들어, Reflect.get
메소드를 사용하여 객체의 프로퍼티 값을 가져올 수 있습니다.
API
- Proxy에 의해 추적 가능한 모든 내부 메서드에는 해당하는 메서드가 Reflect에 있으며, 프록시 트랩과 동일한 이름과 인수를 갖습니다.
// Reflect object methods
Reflect.apply()
Reflect.construct()
Reflect.get()
Reflect.has()
Reflect.ownKeys()
Reflect.set()
Reflect.setPrototypeOf()
Reflect.defineProperty()
Reflect.deleteProperty()
Reflect.getOwnPropertyDescriptor()
Reflect.getPrototypeOf()
Reflect.isExtensible()
예시
- 예제1 - Reflect를 통해, 연산자(new, delete 등)를 함수(Reflect.construct, Reflect.deleteProperty 등) 처럼 호출
const obj = {
name: "John",
age: 30
};
console.log(Reflect.get(obj, "name")); // "John" 출력
- 예제2 - get과 set 트랩. 객체로의 읽기/쓰기 작업을 메시지로 보여주며 투명하게(forward) 전달
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
Reflect를 사용해야 하는 이유
대부분의 경우에는 Reflect 없이도 동일한 작업을 수행할 수 있습니다. 예를 들어, 속성을 읽는 Reflect.get(target, prop, receiver)은 target[prop]로 대체될 수 있습니다.
하지만 아래의 3가지 이유로 인해서 Reflect를 사용해야 하고, Proxy와 함께 사용할때 Reflect를 사용하지 않는다면 어떤일이 벌어지는지는 아래 예시 코드로 넣어 두겠습니다. 이는 3번 확장성과 관련있습니다.
- 표준화된 방식의 코드 작성: Reflect는 ECMAScript 표준의 일부입니다. 따라서 Reflect.get()를 사용하여 코드를 작성하면 코드를 이해하기 쉬우며 유지 보수 및 업그레이드 작업이 용이해집니다.
- 객체 reflection 관련 기능 지원: Reflect는 객체 리플렉션 관련 기능을 제공하는 유용한 메서드들을 모아 놓은 객체입니다. 따라서 객체의 속성을 접근할 때 Reflect.get()을 사용하면 다른 리플렉션 관련 메서드들과의 일관성을 유지할 수 있습니다.
- 확장성: Reflect.get()는 프로퍼티에 대한 접근을 가로챌 수 있는 Proxy 객체에서도 사용할 수 있습니다. Proxy 객체를 사용하여 객체에 대한 커스텀 로직을 추가할 때, Reflect.get()을 사용하면 기존 동작을 유지하면서 커스텀 로직을 추가할 수 있습니다.
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
// return target[prop]; // (*) target = user
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
결론
메타프로그래밍은 프로그램의 유지보수성과 재사용성을 향상시키는 중요한 기술입니다. ES6에서는 Proxy와 Reflect 객체를 통해 메타프로그래밍을 할 수 있습니다. 이를 이용하여 코드의 동작을 수정하거나 객체의 내부 정보에 접근할 수 있습니다. 이를 이용하여 코드를 더욱 유연하고 강력하게 만들 수 있습니다.
하지만, Self-Modification(자기 자신을 수정하는 것)은 코드의 복잡도를 높이고 디버깅을 어렵게 만드는 등 위험성이 높기 때문에 최대한 피하는 것이 좋습니다. 이에 대한 대안으로는 Proxy와 Reflect 객체를 이용하여 동적으로 객체를 생성하고 조작할 수 있습니다.
이상으로 메타프로그래밍, Proxy, Reflect에 대해 간단히 살펴보았습니다. 이를 이용하여 좀 더 유연하고 강력한 코드를 작성할 수 있습니다.