...
JavaScript Proxy 객체
프록시(Proxy)의 사전적 뜻은 '대리인', '대리'라는 뜻이다. 서버를 다뤄본 독자분들이라면 프록시 서버에 대해 질리도록 들어봤을 것이다. 프록시 서버는 클라이언트와 본 서버 중간에 위치하여 캐싱, 분산 등 여러 부가 기능을 수행하는 대리자 역할로서 정말 유용하게 다뤄지는 개념이다.
심지어 디자인 패턴에서는 따로 프록시 패턴(Proxy Pattern) 으로 코드 패턴을 정의하여 소개하기도 한다. 이 프록시 코드 패턴은 실무에서 정말 빈번하게 다뤄지기 때문에, 이에 각각 프로그래밍 언어에서는 별도의 프록시 객체를 만들어서 아예 개발자에게 API로 제공해준다. 대표적으로 자바(Java) 진영의 Dynamic Proxy 를 들 수 있으며, 자바스크립트(JavaScript) 진영에서도 ES2015부터 별도의 Proxy 객체를 지원하기 시작했다.
자바스크립트에서의 Proxy 객체의 역할은 대상 객체을 감싸서(wrapping), 속성 조회, 할당, 열거 및 함수 호출 등 여러 기본 동작을 가로채(trap) 특별한 다른 동작을 가미시키는 대리자 역할을 한다. 대상 객체는 Object, Array 등 자바스크립트의 모든 자료형이 대상이 될 수 있다.
예를들어 obj.name = "홍길동" 과 같은 객체의 프로퍼티에 값을 set 하는 작업을 중간에 가로채어 추가 로직을 가미할 수 있다는 말이다. 가로채진 작업은 Proxy 자체에서 처리되기도 하고, 원래 객체가 처리하도록 그대로 전달되기도 한다.
이처럼 프록시 객체를 이용하면 기존 객체의 속성이나 메서드를 덮어쓸 수 있고, 객체의 속성이나 메서드에 접근하는 행위에 대해 콜백을 먹여, 데이터 검증에 사용하는 Validator로 사용할 수도 있고, 데이터 변화를 감지하는 로거(logger)로도 사용할 수 있다.
Proxy 객체 등록하기
// 대상 객체(Real Subject)
let target = { ... }
// target의 동작을 가로채어 제어하는 미리 정의된 메서드 내부를 구현한 핸들러
let handler = {
get(target, prop) { ... },
set(target, prop, value) { ... },
has(target, prop) { ... },
...
}
// 프록시 생성 및 등록
const proxy = new Proxy(target, handler)
target: Proxy의 대상이 되는 대상 객체handler: target의 동작을 제어하는 메서드를 정의한 객체
handler 객체를 보면, get, set, has ..등 메서드를 정의한 것을 볼 수 있는데, 이 메서드들은 target 객체의 동작을 중간에 가로채서 제어 로직을 가미하게 하는 미리 정의된 메서드들이다. 그래서 이 메서드들을 트랩(trap) 이라고 부른다.
간단하게 프록시 예제를 들어보자면 다음과 같다. 먼저 타겟이 되는 user 객체와 handler를 정의하고 new Proxy()를 통해 프록시 객체를 등록해준다. 그리고 대상 객체의 firstName 과 lastName 을 접근해본다.
// 대상 객체 (target)
const user = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
}
// 핸들러 (handler)
const handler = {
// 대상 객체에 프로퍼티 값을 할당하려는 동작(get)을 가로채어 실행
get(target, property) {
console.log(`Property ${property} has been read.`);
return target[property];
}
}
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.firstName);
console.log(proxyUser.lastName);
이때 주의할점은 클라이언트는 프록시 객체인 proxyUser를 통해 접근해야 된다는 점이다. 왜냐하면 위의 그림과 같이 user 객체를 프록시 객체가 감싸고 있는 형태이기 때문에 외부 포장과 같은 프록시를 통해 대상 객체인 user와 통신하게 되기 때문이다. 콘솔 로그를 실행하면 원래는 값만 딸랑 나와야 되는것이 다음과 같이 추가적으로 코드가 가미됨을 볼 수 있다.
이처럼 Proxy 개체를 사용하면 기본 언어 작업(속성 조회, 할당, 열거, 함수 호출 ..등)에 대한 사용자 지정 동작을 가로채고 추가 재정의할 수 있다.
Proxy 트랩 핸들러 종류
handler로 가로챌 수 있는 동작은 Get 행위 뿐만 아니라 객체의 속성과 함수 그리고 프로토타입을 조작할 수 있다.
Property 가로채는 트랩 | 작동 시점 |
get | 프로퍼티를 읽을 때 |
set | 프로터티에 값을 쓸 때 |
has | in 연산자가 작동할 때 |
deleteProperty | delete 연산자가 작동할 때 |
Method 가로채는 트랩 | 작동 시점 |
apply | 함수를 호출할 때 |
constructor | new 연산자가 작동할 때 |
Object 가로채는 트랩 | 작동 시점 |
getPrototypeOf | Object.getPrototypeOf 작동할 때 |
setPrototypeOf | Object.setPrototypeOf 작동할 때 |
isExtensible | Object.isExtensible 작동할 때 |
preventExtensions | Object.preventExtensions 작동할 때 |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor 작동할 때 |
ownKeys | Object.getOwnPropertyNames 작동할 때 Object.getOwnPropertySymbols 작동할 때 |
get 트랩
- 대상 객체의 프로퍼티를 읽을 때 발동
let obj = {
name : "홍길동"
};
let handler = {
get(target, prop, receiver) {
console.log("프로퍼티 읽을 때 중간에 가로채어 로직 시행");
console.log('target: ', target); // 대상 객체
console.log('prop: ', prop); // 프로퍼티
console.log('receiver: ', receiver); // 프록시 객체 this
return "임꺽정"; // 프록시에서 이름을 바꿔버림
},
};
const proxy = new Proxy(obj, handler);
console.log(proxy.name);
set 트랩
- 대상 객체의 프로퍼티를 쓸 때 발동
- 값을 쓰는 게 성공적으로 처리되었으면 반드시 true를 반환해야 한다. 그렇지 않은 경우는 false를 반환해야 한다.
let obj = {
name : "홍길동"
};
let handler = {
set(target, prop, value, receiver) {
console.log("프로퍼티 쓸 때 중간에 가로채어 로직 시행");
console.log('target: ', target); // 대상 객체
console.log('prop: ', prop); // 프로퍼티 이름
console.log('value: ', value); // 할당한 값
console.log('receiver: ', receiver); // 프록시 객체 this
target[prop] = value * 2;
return true;
},
};
const proxy = new Proxy(obj, handler);
proxy.age = 50;
console.log(proxy.age); // 100
console.log(obj.age); // 100
has 트랩
- 대상 객체의 프로퍼티가 있는지 검사하는
in연산자가 작동할 때 발동
let obj = {
name : "홍길동"
};
let handler = {
has(target, prop) {
console.log("프로퍼티 확인 때 중간에 가로채어 로직 시행");
console.log('target: ', target); // 대상 객체
console.log('prop: ', prop); // 프로퍼티 이름
},
};
const proxy = new Proxy(obj, handler);
if("name" in proxy) {
}
deleteProperty 트랩
- 대상 객체의 프로퍼티를 삭제할 때 발동
- 값을 지우는 게 성공적으로 처리되었으면 반드시 true를 반환해야 한다. 그렇지 않은 경우는 false를 반환해야 한다.
let obj = {
name : "홍길동"
};
let handler = {
deleteProperty(target, prop) {
console.log("프로퍼티 삭제할 때 중간에 가로채어 로직 시행");
console.log('target: ', target); // 대상 객체
console.log('prop: ', prop); // 프로퍼티 이름
console.log("삭제 제한");
return false; // 일부러 false를 반환하게 해서 삭제를 제한 시킴
},
};
const proxy = new Proxy(obj, handler);
delete proxy.name;
console.log(proxy.name); // 삭제가 되지않고 그대로 남음
apply 트랩
- 함수를 호출할 때 발동
let obj = {
name: '홍길동',
print: function () {
console.log(`My Name is ${this.name}`);
},
};
let handler = {
apply(target, thisArg, args) {
console.log('메서드 실행할 때 중간에 가로채어 로직 시행');
console.log('target: ', target); // 대상 함수
console.log('thisArg: ', thisArg); // this의 값
console.log('args: ', args); // 매개변수 목록 (배열)
console.log('이름 바꿔 버리기 ~');
thisArg.name = '임꺽정';
Reflect.apply(target, thisArg, args); // 대상 원본 함수 실행
},
};
// print 함수를 프록시로 감싸기
obj.print = new Proxy(obj.print, handler);
obj.print();
Proxy 해제 하기
만일 쓸모없어진 프록시 객체를 해체하여 메모리 측면에서 여유공간을 얻고싶다면 어떻게 할까?
Proxy 객체의 Target 원본 객체를 null로 해버리면 자동으로 해체될것 같아 보이지만 반영되지 않는다. 따라서 직접 명시적으로 프록시 객체를 제거할 필요가 있다.
프록시를 해지하지 위해선, 처음 부터 취소 가능 프록시(Revocable Proxy)로 정의할 필요가 있는데, Proxy.revocable() 를 통해 얻을 수 있는 revoke 를 이용해서 Target 객체 를 참조하던 Proxy 객체를 해제할 수 있다.
let obj = {
name: '홍길동',
};
let handler = {
get(target, key) {},
set(target, key, value) {},
};
// 취소 가능한 프록시 객체를 구조분해로 얻기
const { proxy, revoke } = Proxy.revocable(obj, handler);
try {
revoke(); // 프록시 해제
proxy.name = "임꺽정"; // ! 프록시 사용 불가 - Cannot perform 'set' on a proxy that has been revoked
} catch (err) {
console.error(err.message);
}
JavaScript Reflect 객체
프로그래밍에서 리플렉션(Reflection)이라는 것은 런타임 단에서 겍체의 변수, 속성 및 메서드를 다이나믹하게 조작하는 프로세스를 말한다.
사실 자바스크립트에서 이미 리플렉션 기능이 있었다. 예를 들어 Object.keys(), Object.getOwnPropertyDescriptor() 및 prototype와 같은 Object 메서드들은 어찌보면 고전적인 리플렉션 기능이라고 말할 수 있다. 하지만 ES6에서는 위와 같은 사용성이 나쁘고 메서드명이 길어, 아예 Refelct 라는 새로운 전역 객체를 추가하였다.
즉, Reflect 객체는 Proxy와 같이 명령을 가로챌 수 있는 메서드를 제공하는 내장 객체로서 좀더 심플화한 객체라고 보면 된다.
Reflect 메소드 종류
Reflect 객체는 생성자가 존재하지 않아 new 인스턴스화를 통해 사용하는 것이 아닌, Math나 JSON 객체처럼 정적 메소드만 지원한다. 그리고 Proxy의 모든 트랩 종류를 동일한 인터페이스로 지원한다.
Reflect 핸들러 | 내부 작동 방식 |
Reflect.get() | 속성 값을 반환 obj[prop] 호출하는 것과 같음 |
Reflect.set() | 속성 값을 할당하면 true 반환 obj[prop] = value 호출하는 것과 같음 |
Reflect.has() | 속성(소유 또는 상속)이 존재하는지 여부를 나타내는 부울을 반환 |
Reflect.deleteProperty() | 속성 값을 삭제 delete obj[prop] 호출하는 것과 같음 |
Reflect.construct() | 생성자 호출 |
Reflect.apply() | 지정된 인수로 함수를 호출 |
Reflect.defineProperty() | 속성이 개체에 있으면 속성의 속성 설명자를 반환하고 그렇지 않으면 정의되지 않은 속성을 반환 |
Reflect.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() 와 같음 |
Reflect.getPrototypeOf() | Object.getPrototypeOf() 와 같음 |
Reflect.isExtensible() | Object.isExtensible() 와 같음 |
Reflect.ownKeys() | 객체의 소유 속성 키(상속되지 않음)의 배열을 반환 |
Reflect.preventExtensions() | Object.preventExtensions() 와 같음 |
Reflect.setPrototypeOf() | 객체의 프로토타입을 설정 |
Reflect.get
- 객체의 속성을 조회
const obj = { a: 1, b: 'zero', c: true };
const arr = [1, 'zero', true];
Reflect.get(obj, 'a'); // 1
Reflect.get(arr, 1); // 'zero'
Reflect.set
- 객체의 속성에 값을 설정
Reflect.set(obj, 'd', ['arg1', 'arg2']); // true 반환
obj.d; // ['arg1', 'arg2']
Reflect.has
- 객체가 해당 속성을 가졌는지 검사
Reflect.has(obj, 'b'); // true
Reflect.apply
- 함수를 실행
- 두 번째 인자는 함수의 this를 바꾸고 싶을 때 사용하는 거라서, 보통은 null을 넣어준니다.
- 세 번째 인자는 함수에 넣을 인자이다.
- call, apply, bind 와 사용법이 비슷하다고 보면 된다.
const add = (a, b) => a + b;
Reflect.apply(add, null, [3, 5]); // 8
Reflect.construct
- 생성자
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const p = Reflect.construct(Person, ['홍길동', 55]); // new Person('홍길동', 55)
Reflect.ownKeys
- 객체의 속성명들을 배열로 반환
- 단, 상속받은 속성과 enumerable하지 않은 것은 제외
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
Reflect.ownKeys(duck); // [ "name", "color", "greeting" ]
Proxy + Reflect 조합하기
자바스크립트 Proxy 객체를 이용해 [[GET]] 트랩 핸들러를 설정하였다. 만일 Target Object 에서 [[Get]] 연산이 발생하면, Proxy Handler 에 정의한 get 함수가 먼저 호출하게 될 것이다. 따라서 get 트랩 함수에 정의한 내용에 따라 Target Object 의 [[Get]] 연산과 별개로 동작이 가능하다.
만약 Target Object 의 [[Get]] 연산을 이어나가고 싶을때 우리는 매개변수를 이용해 target[prop] = value 로 처리 하였었다. 하지만 일반적인 Proxy의 트랩 파라미터를 이용한 방법은 현재 일어나는 탐색의 주체가 무엇인지 알 수가 없기 때문에 사이드 이펙트 문제가 발생하게 된다. 그래서 실무에서 아래와 같이 Reflect 메소드로 조합해서 사용한다.
Reflect는 Proxy에서 트랩할 수 있는 모든 내부 메서드와 동일한 내장 메서드를 고대로 가지고 있다. 따라서 Reflect를 사용하면 원래 객체에 그대로 작업을 전달할 때 별도의 사이드 이펙트 없이 전달 가능하다는 장점이 있다.
Proxy / Reflect 응용 예제 모음
값을 할당할때 문자열 필터링 하기
만일 Person 객체의 이름을 지정하는데 있어 숫자가 들어오거나 10자리 이상 긴 문자열이 들어올경우 이를 필터링하고 싶은데 부득이하게 Person 객체의 멤버를 수정할 수 없을 경우 프록시를 이용해 추가 로직을 가미 시킬 수 있다.
class Person {
name;
setName(name) {
this.name = name;
}
}
let me = new Person();
me.setName(123123); // 에러가 뜨게 하고 싶다.
me.setName('asdjhfkjasdhflkjshadfjhsadkfjl'); // 에러가 뜨게 하고 싶다.
setName() 이라는 메서드 실행 로직을 가로채어 가미해야 되기 때문에 apply 트랩 핸들러를 이용하여 다음과 같이 구현할 수 있다.
class Person {
name;
setName(name) {
this.name = name;
}
}
let me = new Person();
me.setName = new Proxy(me.setName, {
apply(target, thisArg, args) {
const value = args[0];
if (typeof value !== 'string') {
throw new TypeError('이름은 문자열 데이터만 가능합니다.');
} else if (value.length > 10) {
throw new RangeError('이름은 최대 10글자 이내 여야 합니다.');
}
return Reflect.apply(target, thisArg, args);
}
})
try {
me.setName(123123);
} catch (e) {
console.error(e.message);
}
try {
me.setName('asdjhfkjasdhflkjshadfjhsadkfjl');
} catch (e) {
console.error(e.message);
}
me.setName('inpa');
console.log(me.name);
존재하지 않은 프로퍼티는 에러 발생 시키기
자바스크립트는 유연한 언어이기 때문에 객체에 등록되지 않은 프로퍼티라도 접근하면 그냥 undefined를 반환하고 만다. 하지만 이는 오히려 개발 생산성을 줄어들게 하는 원인이다. 따라서 이를 타이트하게 관리하기 위하여 존재하지 않은 프로퍼티를 접근할경우 에러를 던지게 구현하고 싶다.
let user = {
name: 'inpa',
};
console.log(user.name); // inpa
console.log(user.age); // undefined
get 트랩에서 중간에 가로채어 in 연산자를 이용해 prop에 대한 존재 여부 검사를 분기해주면 된다.
let user = {
name: 'inpa',
};
user = new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
} else {
throw new Error(`ReferenceError: Property doesn't exist "${prop}"`);
}
},
});
console.log(user.name); // inpa
console.log(user.age); // ReferenceError: Property doesn't exist "age"
음수 인덱스 구현하기
자바스크립트의 배열 인덱스는 파이썬 과는 달리 음수 인덱스를 지원하지 않는다. 하지만 이를 프록시를 이용해 구현할 수 있다.
let array = ["홍길동", "임꺽정", "박혁거세"];
// 마지막 요소 : 박혁거세
console.log('array[-1]: ', array[-1]);
// 뒤에서 두 번째 요소 : 임꺽정
console.log('array[-2]: ', array[-2]);
// 뒤에서 세 번째 요소 : 홍길동
console.log('array[-3]: ', array[-3]);
간단하게 프록시 get 트랩에 prop 매개변수에 담기는 인덱스 숫자가 0보다 작을 경우 배열 length와 적절한 계산으로 정상 인덱스로 변환한뒤 그대로 실행해주면 되는 일이다.
let array = ["홍길동", "임꺽정", "박혁거세"];
array = new Proxy(array, {
get(target, prop, receiver) {
let propNum = Number(prop); // 문자를 숫자로 변환
if(propNum < 0) {
propNum += target.length;
}
return Reflect.get(target, propNum, receiver);
}
})
// 마지막 요소 : 박혁거세
console.log('array[-1]: ', array[-1]);
// 뒤에서 두 번째 요소 : 임꺽정
console.log('array[-2]: ', array[-2]);
// 뒤에서 세 번째 요소 : 홍길동
console.log('array[-3]: ', array[-3]);
# 참고자료
https://www.javascripttutorial.net/es6/javascript-proxy/
https://www.zerocho.com/category/ECMAScript/post/57ce7fec2a00e600151f085c
https://brunch.co.kr/@skykamja24/644
https://ko.javascript.info/proxy
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.