Proxy?
객체에 대한 작업을 가로채서 대신 처리할 수 있는 자바스크립트의 기능임
간단하게 말하면 아파트의 경비원과 같은 역할로 설명이 가능함
아파트에 들어가거나 나올때는 항상 경비원을 거쳐야함
방문자를 확인해서 기록하거나 필요시 출입에 대한 제한이 가능함
기본 문법
code
/**
* 인터페이스 정의
*/
interface ProxyConstructor {
/**
* Creates a revocable Proxy object.
* @param target A target object to wrap with Proxy.
* @param handler An object whose properties define the behavior of Proxy when an operation is attempted on it.
*/
revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void };
/**
* Creates a Proxy object. The Proxy object allows you to create an object that can be used in place of the
* original object, but which may redefine fundamental Object operations like getting, setting, and defining
* properties. Proxy objects are commonly used to log property accesses, validate, format, or sanitize inputs.
* @param target A target object to wrap with Proxy.
* @param handler An object whose properties define the behavior of Proxy when an operation is attempted on it.
*/
new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
/**
* 활용 예제
*/
const target = {};
const handler = {};
const proxy = new Proxy(target, handler);프록시 트랩
핸들러에 정의하는 메소드를
트랩이라고 부름이는 작업을 가로챈다는 의미임
get 트랩 : 속성을 읽는 경우
code
type Product = {
name: string;
price: number;
};
const product: Product = {
name: "맥북 프로 16",
price: 2_000_000,
};
const productProxy = new Proxy(product, {
get(target, prop: keyof Product) {
console.log(`${prop}에 접근함`);
return target[prop];
},
});
console.log(productProxy.name);
// name에 접근함
// 맥북 프로 16set 트랩 : 속성을 수정하는 경우
code
type Product = {
name: string;
price: number;
};
const product: Product = {
name: "맥북 프로 16",
price: 2_000_000,
};
const productProxy = new Proxy(product, {
set(target, prop: keyof Product, value: unknown) {
console.log(`${prop}에 ${value}를 할당함`);
(target as Record<keyof Product, unknown>)[prop] = value;
return true;
},
});
productProxy.name = "맥미니 m4";
// name에 맥미니 m4를 할당함has 트랩 : in 연산자를 사용하는 경우
code
type Product = {
name: string;
price: number;
_serialNumber: string;
};
const product: Product = {
name: "맥북 프로 16",
price: 2_000_000,
_serialNumber: "1234567890",
};
const productProxy = new Proxy(product, {
has(target, prop: keyof Product) {
/**
* 프로퍼티가 _로 시작한다면 해당 속성은 숨기기
*/
if (prop.startsWith("_")) {
return false;
}
return prop in target;
},
});
console.log("name" in productProxy); // true
console.log("_serialNumber" in productProxy); // false예제
유효성 검사 추가하기
code
type User = {
name: string;
age: number;
};
const user: User = {
name: "Dongwoo",
age: 27,
};
const userProxy = new Proxy(user, {
set(target, prop: keyof User, value: unknown) {
if (prop === "age") {
if (typeof value !== "number") {
throw new Error("나이는 숫자로 설정해야함");
}
if (value < 0 || value > 100) {
throw new Error("나이는 0~100 사이의 숫자로 설정해야함");
}
}
(target as Record<keyof User, unknown>)[prop] = value;
return true;
},
});
userProxy.age = 28;
console.log(userProxy.age); // 28
// Error: 나이는 0~100 사이의 숫자로 설정해야함
// at Object.set (file:///Users/imkdw/Desktop/ts-pg/dist/node/node.js:12:23)
// at file:///Users/imkdw/Desktop/ts-pg/dist/node/node.js:21:15
// at ModuleJob.run (node:internal/modules/esm/module_job:271:25)
// at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:578:26)
// at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:116:5)
userProxy.age = -1;옵셔널 프로퍼티에 기본값 제공하기
code
type Theme = "light" | "dark";
type Language = "ko" | "en";
type Config = {
theme?: Theme;
language?: Language;
};
const defaultConfig: Config = {
theme: "dark",
language: "ko",
};
function withDefaultConfig<T extends object>(target: T, defaultConfig: Partial<T>): T {
return new Proxy(target, {
get(target, prop: string | symbol) {
if (typeof prop === "symbol") {
return (target as any)[prop];
}
const value = target[prop as keyof T];
if (value === undefined || value === null) {
return defaultConfig[prop as keyof T];
}
return value;
},
});
}
const userConfig: Config = {};
const config = withDefaultConfig(userConfig, defaultConfig);
/**
* 초기 userConfig에는 아무런 값도 존재하지 않음
* 하지만 Proxy를 통해 값이 없으면 defaultConfig의 값이 반환되도록 설정됨
*/
console.log(config.theme); // dark
console.log(config.language); // ko
/**
* userConfig에 theme 값을 설정하면 해당 값을 그대로 반환함
*/
config.theme = "light";
console.log(config.theme); // light음수 인덱스 배열 제공하기
파이썬과 유사하게 음시 인덱스로 배열 끝에서부터 스캔하도록 만들 수 있음
code
function createNegativeArray<T>(array: T[]): T[] {
return new Proxy(array, {
get(target, prop: string) {
const index = parseInt(prop, 10);
if (Number.isNaN(index)) {
return (target as any)[prop];
}
if (index < 0) {
return target[target.length + index];
}
return target[index];
},
});
}
const array = createNegativeArray(["a", "b", "c", "d", "e"]);
console.log(array[0]); // 'a'
console.log(array[-1]); // 'e'
console.log(array[-2]); // 'd'
const arr = [1, 2, 3];
console.log(arr[-1]); // undefined취소가 가능한 프록시 : Proxy.revocable
프록시의 경우 한번 생성하면 영구적으로 동작함
하지만
Proxy.revocable을 통해서 나중에 비활성화가 가능한 프록시를 만들 수 있음
일회용 객체 만들기
code
type Coupon = {
code: string;
discount: number;
use: () => string;
};
function createOneTimeCoupon(coupon: Coupon) {
const { proxy, revoke } = Proxy.revocable(coupon, {
get(target, prop) {
const value = (target as any)[prop];
if (prop === "use" && typeof value === "function") {
return () => {
const result = value.call(target);
revoke();
return result;
};
}
return value;
},
});
return proxy;
}
const coupon = createOneTimeCoupon({
code: "SAVE20",
discount: 20,
use: () => "쿠폰이 적용되었습니다",
});
console.log(coupon.code); // 'SAVE20'
console.log(coupon.use()); // '쿠폰이 적용되었습니다'
console.log(coupon.code); // TypeError: Cannot perform 'get' on a proxy that has been revoked