프록시(Proxy)라는 건 네트워크나 CORS 에러 처리 때 들어본 개념이다. 자바스크립트 언어 자체에도 프록시라는 개념이 있다니 신기했다. 프록시는 어디에 쓰이든 항상 '중간'에서 뭔가를 해주는 역할인 것 같았다. MDN의 프록시의 정의부터 시작해보자.
프록시 객체는 조회, 할당, 열거, 호출 등의 기본적인 작업들에 대해 사용자(개발자) 지정 동작을 추가로 정의하는 데 사용된다.
한 마디로 프록시를 사용하면 개발의 본 흐름 도중에 추가로 동작을 정의(대문자화, 값 수정, 에러 시 콘솔 출력 등)하기가 매우 용이하다.
아래에서 좀 더 자세히 알아보자.
프록시 객체의 형태는 다음과 같이 두 개의 인자를 받는다.
var x = new Proxy(target, handler);
// 프록시를 적용할 객체 (target)
const dog = { breed: "German Shepherd", age: 5 };
// 프록시 생성
const dogProxy = new Proxy(dog, {
// handler 객체에 게터와 세터 정의
get(target, breed) {
return target[breed].toUpperCase(); // breed를 대문자화하여 반환
},
set(target, breed, value) {
console.log("breed 수정");
target[breed] = value; // '할당'으로 들어온 value값으로 breed 수정
},
});
// 객체의 프로퍼티를 출력하는 형태가 '할당'인지 여부에 따라 get이 호출될지, set이 호출될지가 결정된다.
console.log(dogProxy.breed); // 할당이 아니므로 get 호출 (대문자 GERMAN SHEPHERD 반환)
console.log(dogProxy.breed = 'Labrador'); // 할당이므로 set 호출 (breed 수정)
console.log(dogProxy.breed); // 다시 get 호출 (LABRADOR 반환)
프록시는 다음과 같은 경우에 활용하면 유용하다.
// 프록시에서 handler로 쓰일 함수(age 검증)를 '객체의 메서드' 형태로 선언
const validateAge = {
set: function(object, property, value) { // '할당'으로 호출하면 자동으로 이 세 값을 인자로 받음
// 검사하려는 property가 'age'인지를 먼저 검증
if (property === 'age') {
// 새롭게 수정할 age값(value)이 18 이상인지 검증
if (value < 18) {
throw new Error('성인이 아닙니다!');
} else {
object[property] = value; // 객체의 age 프로퍼티의 값을 value로 지정하고,
return true; // true 반환 (프로퍼티 수정에 성공했다는 뜻 - 또다른 곳에서 검증에 사용할 때)
}
}
}
};
// 프록시 생성 (target은 빈 객체, handler는 위에 정의한 validateAge 함수)
const user = new Proxy({}, validateAge);
// '할당'을 하면 handler의 set(세터)을 호출
user.age = 17; // Uncaught Error: 성인이 아닙니다!
user.age = 21; // (성인이 맞으므로) 21로 설정 성공 (실제 호출하려면 get(게터) 필요)
이 경우는 프록시를 활용하지 않았을 때와 프록시를 활용했을 때를 비교해서 살펴보겠다.
먼저 프록시를 활용하지 않은 상황이다.
// 객체인데 '클래스' 역할이라서 get set을 모두 포함하는 dog 객체이다.
// 아직은 프로퍼티가 _name, _age밖에 없지만 더 늘어나면 그만큼 get set을 추가로 정의해야 할 것이다.
const dog = {
_name: 'pup',
_age: 7,
get name() {
console.log(this._name);
},
get age() {
console.log(this._age);
},
set name(newName) {
this._name = newName;
console.log(this._name);
},
set age(newAge) {
this._age = newAge;
console.log(this._age);
}
};
dog.name; // pup (get 호출)
dog.age; // 7 (get 호출)
dog.breed; // 없는 프로퍼티이므로 undefined
dog.name = 'Max'; // 할당을 통해 수정 (set 호출)
dog.name; // (수정 후) Max (get 호출)
프로퍼티 앞에 "_(언더바)"를 붙이면 자바스크립트의 관습상 private(클래스 내부에서만 접근이 가능한 속성)이라는 의미이다.
get set 이름과 객체의 프로퍼티 이름이 같으면 set 안의 this가 세터를 계속 호출하여 무한 루프가 발생한다.
set name(newName) {
this.name = newName; // 세터(name)를 다시 무한 호출
}
프로퍼티 앞의 언더바는 위와 같은 상황을 방지하기 위한 네이밍 관습이다.
만약 get set의 이름을 'getname, rename' 식으로 프로퍼티의 이름과 겹치지 않게 지으면 언더바를 쓰지 않아도 된다.
set rename(newName) {
this.name = newName;
}
이제 프록시 개념을 활용하여 위의 중복된 형태의 코드들을 handler로 합쳐보자.
const dog = {
name: 'pup',
age: 7,
};
// 위에서 쭉 늘어져있던 get set들을 handler를 통해 하나로 합쳤다.
const handler = {
get: (target, property) => {
// in으로 property가 있는지 먼저 검증
property in target ? console.log(target[property]) : console.log('객체에 해당 property 없음');
},
set: (target, property, value) => {
target[property] = value;
console.log('잘 수정됨: ' + target[property]);
},
};
// 프록시 생성
const dogProxy = new Proxy(dog, handler);
dogProxy.name; // pup
dogProxy.age; // 7
dogProxy.name = 'Max'; // 잘 수정됨: Max
dogProxy.age = 8; // 잘 수정됨: 8
dogProxy.breed; // 객체에 해당 property 없음
이제 프로퍼티가 아무리 늘어나도 handler가 프로퍼티의 모든 get set을 처리해준다 👏
이렇게 프록시를 활용하면 get set을 사용하기 전에 데이터를 검증하거나 콘솔 출력과 같은 추가적인 동작을 편하게 지정할 수 있다. 또 handler 개념을 활용하여 중복되는 형태의 get set을 간결하게 처리할 수도 있다.