프록시 패턴
프록시 패턴은 구조적 디자인 패턴으로 프런트엔드 개발에서 널리 사용되는 여러 디자인 패턴 중 하나이며, 많은 오픈 소스 소프트웨어 라이브러리에서 그 사용을 볼 수 있습니다.
프록시, 기본 개념
프록시 패턴은 다른 객체가 이 객체에 대한 액세스를 제어할 수 있도록 프록시를 제공하는 것입니다.
예를 들어 일부 작업은 자주 트리거하고 싶지 않거나 트리거 빈도를 제한해야 하는 경우, 데이터를 조작할 때 Vue의 양방향 데이터 바인딩과 같은 추가 작업을 수행해야 하는 경우와 같은 전형적인 프런트엔드 시나리오를 예로 들어 보겠습니다.
프록시 패턴은 언제 사용하는 것이 적절할까요? - 객체에 대한 액세스를 어느 정도 제어하고 싶을 때.
프록시 모델의 UML 다이어그램은 아래와 같습니다:
코드 샘플
interface Subject {
profit(number: number): void;
}
class RealSubject implements Subject {
profit(number: number): void {
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
console.log(`you can earn money ${number} every day`);
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}
}
class ProxySubject implements Subject {
private readSubject = new RealSubject();
profit(number: number): void {
if (number <= 0) {
console.warn("salary must bigger than zero");
return;
}
this.readSubject.profit(number);
}
}
(function bootstrap() {
const sub = new ProxySubject();
for (let i = 0; i < 10; i++) {
const rnd = Math.random();
sub.profit(rnd > 0.5 ? Number.parseInt((rnd * 1000).toFixed(0)) : 0);
}
})();
프론트엔드 개발의 관행은
위의 코드는 일반적인 구현이지만 프런트엔드에서는 프록시 패턴을 더 쉽게 구현할 수 있는 몇 가지 편리한 API를 제공합니다.
ES5에서는 객체에 게터와 세터를 정의할 때 객체에 일부 제어권을 부여하여 프록시 효과를 얻거나 클로저 + 고차 함수를 사용하여 이를 달성하기 위해 Object.defineProperty를 사용할 수 있습니다.
ES6에는 구문 설탕을 사용하여 프록시 모델을 구현하는 데 직접 사용할 수 있는 Proxy가 추가되었지만, 이 둘을 조합하면 this가 원하는 방식으로 가리키도록 보장하므로 Proxy를 사용할 때는 항상 Reflect API를 함께 사용하도록 하 세요.
네거티브 배열에 대한 인덱스 가져오기 구현하기
프록시는 음수 인덱스가 있는 배열을 구현하는 데 사용할 수 있습니다.
function SafetyArray(arr) {
return new Proxy(arr, {
get(target, propKey, receiver) {
let index = Number(propKey);
// propKey가 음수 인덱스인 경우 양수 인덱스로 변환합니다.
if (index < 0) {
index = target.length + index;
}
// Reflect 사용.get이렇게 하면 어떤 경우에도 "이것"이 의도하지 않은 시나리오의 프록시 객체를 가리키지 않는 등 예상되는 것을 가리키도록 보장합니다.
return Reflect.get(target, index, receiver);
},
set(target, propKey, value, receiver) {
let index = Number(propKey);
// 배열에 숫자 이외의 키를 허용하지 마십시오.
if (Number.isNaN(index) && propKey !== "length") {
return false;
}
// propKey가 음수 인덱스인 경우 양수 인덱스로 변환합니다.
if (index < 0) {
index = target.length + index;
}
// Reflect 사용.get의도하지 않은 시나리오의 프록시 객체를 가리키지 않는 등 어떤 경우에도 "이것"이 예상되는 것을 가리키도록 보장합니다.
return Reflect.set(target, index, value, receiver);
},
});
}
안전한 가치 선택기
위의 아이디어를 사용하면 프록시 패턴을 사용하여 안전한 페처를 구현할 수도 있습니다 (설명 : 안전한 페처 란 무엇입니까?). JS는 동적 언어이기 때문에 코드 작성 시에는 객체 값으로 예상되지만 런타임에는 일부 오류로 인해 읽을 수 없으므로 정의되지 않은 값을 가져온 다음 오류 위의 정의되지 않은 값을 가져 오려고 할 수 있으며, 오류를 방지하기 위해 조기 반환시 대상 객체가 정의되지 않은 것을 발견하면 함수가 안전한 평가자 방법을 사용 )와 같은 lodash가 제공하는 get 함수를 사용하면 안전한 평가자를 구현하는 데 함수를 사용할 수 있습니다. 예를 들어, lodash는 get과 같은 함수를 제공합니다.
/**
* 객체 o에서 키 p로 값에 대한 안전한 액세스
* @param {Object} o
* @param {String} p p .b.c .a[o][d].e이러한 양식은[]이러한 형태의 페치는 예상대로 전달되지 않으면 의도하지 않은 결과로 파싱될 수 있습니다.
*/
function safetyGetProperty(o, p) {
// 비 참조 유형은 오류를 직접 보고합니다.
if (!isRef(o)) {
throw new Error("o must be a reference type");
}
p = String(p);
// 키가 현재 객체에 존재하지 않으면 사용자가 후속 프로세스를 계속하기 전에 복잡한 키의 내용을 전달했음을 의미하며, 그렇지 않으면 직접 값을 가져올 수 있습니다.
if (o && o.hasOwnProperty(p)) {
return o[p];
}
// 키 파싱
const props = parseProps(p);
let prop = props.shift();
let target = o[prop];
// 대상 값이 실제 값이 아닌 경우 루프를 계속하면 오류가 보고되고, 실제 키의 길이가 여전히 존재하면 키 값을 가져오지 않았음을 의미하므로 루프를 계속 반복해야 합니다.
while (target && props.length) {
prop = props.shift();
target = target[prop];
}
// 키의 값이 소진되면 정상 종료이고, 그렇지 않으면 비정상 종료이며 null이 반환됩니다.
return props.length === 0 ? target : null;
}
/**
* 객체 o에 키 p를 사용하여 값 v를 설정하는 것이 안전합니다.
* @param {Object} o
* @param {String} p
* @param {any} v
*/
function safetySetProperty(
o,
p,
v,
propDesc = {
enumerable: true,
writable: true,
configurable: true,
}
) {
// 비 참조 유형은 오류를 직접 보고합니다.
if (!isRef(o)) {
throw new Error("o must be a reference type");
}
p = String(p);
// 프록시 파싱
const realKeys = parseProps(p);
let target = o;
let prop = realKeys.shift();
while (realKeys.length) {
// 키가 순전히 숫자인지 여부
let isPureNumProp = /\d+/.test(prop);
// 객체가 존재하지 않는 경우
if (!target[prop]) {
// 키가 순전히 숫자인 경우 배열로 초기화하고, 그렇지 않으면 원시 객체로 초기화합니다.
target[prop] = isPureNumProp ? [] : {};
}
// 거꾸로 반복하기
target = target[prop];
prop = realKeys.shift();
}
Object.defineProperty(target, prop, {
...propDesc,
value: v,
});
}
/**
* 참조 유형 여부 결정
* @param {Array | Object} o
* @returns
*/
function isRef(o) {
return ["Object", "Array"].some((key) => {
return Object.prototype.toString.call(o, key) === `[object ${key}]`;
});
}
function parseProps(prop) {
// .공식적인 분할, 마지막 문자가.는 가져올 마지막 키스트로크''로 간주되며, 첫 번째 키스트로크가.,그런 다음 첫 번째 키 값의 일부로 취급됩니다.
const primaryKeys = prop.split(".");
if (/^\./.test(prop)) {
// 널 값 팝업
primaryKeys.shift();
// 진리 값을 취하고.첫 번째 키의 일부로 간주
const tmp = primaryKeys.shift();
primaryKeys.unshift("." + tmp);
}
const parsedProps = [];
for (let i = 0; i < primaryKeys.length; i++) {
const key = primaryKeys[i];
if (/\[[\w]+\]/.test(key)) {
const keyGroup = parseSquareBrackets(key);
parsedProps.push(...keyGroup);
} else {
parsedProps.push(key);
}
}
return parsedProps;
}
/**
* 대괄호 안의 키 값 파싱하기
* @param {String} prop
*/
function parseSquareBrackets(prop) {
let pos = 0;
let str = "";
let parsedKeys = [];
// 파싱에서 토큰 정의하기
let parsing = false;
while (pos < prop.length) {
const char = prop[pos++];
// 첫 번째`[`현재[키에 포함되지 않음
if (char === "[") {
if (str != "") {
parsedKeys.push(str);
str = "";
}
parsing = true;
continue;
}
// `]`키는 해결된 것으로 간주됩니다.
else if (char === "]" && parsing) {
parsing = false;
parsedKeys.push(str);
str = "";
} else {
// 극단적인 경우 싱글`]`,시작하기도 전에 이미 다음과 같은 문제에 직면하게 됩니다.]
str += char;
}
}
// 익스트림 케이스 싱글`[`
if (parsing) {
const tmp = parsedKeys.pop();
parsedKeys.push(tmp + "[" + str);
str = "";
}
// 극단적인 경우 싱글`]`
if (str != "") {
parsedKeys.push(str);
str = "";
}
return parsedKeys;
}
function createSafetyObject(ref = {}) {
return new Proxy(ref, {
get(target, prop) {
return safetyGetProperty(target, prop)
},
set(target, prop, newValue) {
return safetySetProperty(target, prop, newValue)
}
})
}
// 객체에서 정의해 보세요.
// Object.defineProperty(Object, 'createSafety', {
// value: createSafetyObject
// })
const obj = createSafetyObject();
obj["c[0].d"] = 2
obj.bbb = 10;
console.log(obj.c[0].d) // 2
console.log(obj.bbb) // 10
이미지의 지연 로딩
ProxyAPI를 사용하지 않고 이미지 로딩을 구현합니다:
interface IImage {
display(): void;
}
class RealImage implements IImage {
private url: string;
constructor(url: string) {
this.url = url;
}
loadImage(): void {
console.log(` ${this.url} 이미지 로드`);
let img = document.createElement('img');
img.src = this.url;
document.body.appendChild(img); // DOM에 이미지 추가하기
}
display(): void {
this.loadImage();
console.log('이미지 표시');
}
}
class ImageProxy implements IImage {
private realImage: RealImage | null = null;
private url: string;
constructor(url: string) {
this.url = url;
}
display(): void {
if (!this.realImage) {
console.log('이미지에 먼저 액세스하고 이제 로딩을 시작하십시오....');
this.realImage = new RealImage(this.url);
this.realImage.display();
}
}
}
// 요소가 뷰포트에 있는지 확인하기
function isInViewport(element: HTMLElement) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
// 스크롤 이벤트 처리
function onScroll() {
document.querySelectorAll('img[data-src]').forEach(imgElement => {
if (isInViewport(imgElement as HTMLElement) && !imgElement.src) {
const imgUrl = imgElement.getAttribute('data-src');
if (imgUrl) {
const lazyImage = new ImageProxy(imgUrl);
lazyImage.display();
}
}
});
}
// 스크롤 이벤트에 리스너 추가하기
window.addEventListener('scroll', onScroll);
// 초기 로드 시에도 검사가 수행됩니다.
onScroll();
프록시를 사용하여 이미지 로딩을 구현합니다:
class RealImage {
imgElement: HTMLImageElement;
constructor(imgElement: HTMLImageElement) {
this.imgElement = imgElement;
}
display(): void {
const imgUrl = this.imgElement.getAttribute('data-src');
if (imgUrl) {
console.log(` ${imgUrl} 이미지 로드`);
this.imgElement.src = imgUrl;
this.imgElement.onload = () => console.log('이미지 로드됨');
this.imgElement.onerror = () => console.error('이미지를 로드하지 못했습니다');
}
}
}
// 프록시 핸들러 생성하기
const ImageProxyHandler: ProxyHandler<RealImage> = {
get: function(target, prop, receiver) {
if (prop === "display") {
return function() {
if (!target.imgElement.src) { // 이미지가 로드되지 않은 경우 디스플레이 메서드를 호출하여 이미지를 로드합니다.
target.display();
}
};
}
return Reflect.get(target, prop, receiver);
}
};
// 요소가 뷰포트에 있는지 확인하기
function isInViewport(element: HTMLElement) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
// 스크롤 이벤트 핸들러
function onScroll() {
document.querySelectorAll('img[data-src]').forEach(imgElement => {
if (isInViewport(imgElement as HTMLElement)) {
const realImage = new RealImage(imgElement as HTMLImageElement);
const lazyImage = new Proxy(realImage, ImageProxyHandler);
lazyImage.display();
}
});
}
// 스크롤 이벤트 리스너 추가하기
window.addEventListener('scroll', onScroll);
// 이미지도 초기 로딩 중에 확인됩니다.
onScroll();
통합 오류 캡처
NestJS 소스 코드에서는 많은 디자인이 통합 예외 처리를 위해 프록시 패턴을 사용하므로 코드가 더욱 강력하고 안정적입니다.
다음은 이러한 장소 중 한 곳의 예이며, 핵심 코드를 발췌하여 보여드리겠습니다:
class NestFactoryStatic {
public async create() {
// 코드에서 발췌한 부분
const instance = new NestApplication();
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}
private createNestInstance<T>(instance: T): T {
return this.createProxy(instance);
}
private createProxy(target: any) {
const proxy = this.createExceptionProxy();
return new Proxy(target, {
get: proxy,
set: proxy,
});
}
private createExceptionProxy() {
return (receiver: Record<string, any>, prop: string) => {
if (!(prop in receiver)) {
return;
}
if (isFunction(receiver[prop])) {
// 가능한 오류 트래핑 수행
return this.createExceptionZone(receiver, prop);
}
// 프로퍼티에 대한 액세스가 직접 릴리스됩니다.
return receiver[prop];
};
}
}
함수 결과 캐싱
실제 개발에서 일부 작업은 시스템 리소스를 더 많이 소비 할 수 있으므로 캐시에 사용하여 소프트웨어의 전반적인 효율성을 향상시킬 수 있으므로 다음은 예제의 캐시 기능의 결과를 얻기 위해 프록시 모드를 사용하는 것이며, lodash도 이러한 API를 제공합니다-> memoize.
function createMemoizedFunction(func) {
const cache = new Map();
return new Proxy(func, {
apply(target, thisArg, args) {
// 함수의 매개변수를 기반으로 고유한 캐시 키를 생성합니다.
const cacheKey = args.toString();
if (cache.has(cacheKey)) {
console.log('캐시에서 결과 가져오기');
return cache.get(cacheKey);
}
console.log('결과를 계산하고 캐시하기');
const result = target.apply(thisArg, args);
cache.set(cacheKey, result);
return result;
}
});
}
// 예제 함수: 두 숫자의 합 계산하기
function add(a, b) {
return a + b;
}
// 추가 기능의 메모화된 버전 만들기
const memoizedAdd = createMemoizedFunction(add);
// 암기된 함수 사용
console.log(memoizedAdd(2, 3)); // 결과 계산 및 캐싱
console.log(memoizedAdd(2, 3)); // 캐시에서 결과 가져오기
console.log(memoizedAdd(4, 5)); // 결과 계산 및 캐싱
요약
위에서 언급한 프록시 패턴과 데코레이션 패턴에는 몇 가지 유사점과 차이점이 있습니다. 새 클래스를 사용하여 기존 객체를 래핑하고 새 클래스는 원래 객체와 동일한 인터페이스를 구현하며 이 인터페이스를 통해 원래 객체에 대한 호출을 위임한다는 공통점을 공유합니다. 그러나 프록시 패턴은 접근 제어를 강조하고 데코레이션 패턴은 다음을 추가하는 기능을 강조합니다.
실제 프런트엔드 개발에서는 프록시 모델을 더 간단하게 구현하기 위해 프록시를 광범위하게 사용할 수 있습니다.





