blog

프론트엔드 개발 실무에서의 디자인 패턴 - 프록시 패턴

프록시 패턴 프록시 패턴은 구조적 디자인 패턴이지만 많은 오픈 소스 소프트웨어 라이브러리에서 프록시 패턴의 사용 내에서 볼 수있는 광범위한 여러 디자인 패턴의 프런트 엔드 개발에서...

Oct 23, 2025 · 10 min. read
シェア

프록시 패턴

프록시 패턴은 구조적 디자인 패턴으로 프런트엔드 개발에서 널리 사용되는 여러 디자인 패턴 중 하나이며, 많은 오픈 소스 소프트웨어 라이브러리에서 그 사용을 볼 수 있습니다.

프록시, 기본 개념

프록시 패턴은 다른 객체가 이 객체에 대한 액세스를 제어할 수 있도록 프록시를 제공하는 것입니다.

예를 들어 일부 작업은 자주 트리거하고 싶지 않거나 트리거 빈도를 제한해야 하는 경우, 데이터를 조작할 때 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)); // 결과 계산 및 캐싱

요약

위에서 언급한 프록시 패턴과 데코레이션 패턴에는 몇 가지 유사점과 차이점이 있습니다. 새 클래스를 사용하여 기존 객체를 래핑하고 새 클래스는 원래 객체와 동일한 인터페이스를 구현하며 이 인터페이스를 통해 원래 객체에 대한 호출을 위임한다는 공통점을 공유합니다. 그러나 프록시 패턴은 접근 제어를 강조하고 데코레이션 패턴은 다음을 추가하는 기능을 강조합니다.

실제 프런트엔드 개발에서는 프록시 모델을 더 간단하게 구현하기 위해 프록시를 광범위하게 사용할 수 있습니다.

Read next

영상 융합 플랫폼 지능형 엣지 분석 올인원 카메라 이상 변위 식별 알고리즘 적용 의의

시대가 발전하고 기술이 발전하면서 영상 감시 시스템은 공공 안전과 기업 자산 보안을 유지하는 중요한 도구가 되었습니다. 영상 감시 분야에서는 정확한 정보를 얻기 위해 카메라의 선명도와 안정성이 매우 중요합니다. 그러나 비정상적인 카메라 변위는 감시의 선명도에 영향을 미치는 일반적인 문제입니다.

Oct 22, 2025 · 3 min read