맨틀 이야기

자바스크립트의 프록시 객체 본문

JavaScript

자바스크립트의 프록시 객체

jbilee 2024. 7. 15. 16:00

자바스크립트에는 프록시라는 객체가 있다. 어떤 객체를 생성할 때, 그 객체가 만들어지는 과정 도중에 난입해서 객체의 요소들에 영향을 줄 수 있는게 프록시다.

 

프록시 객체는 변형할 타겟 객체와 변형 액션을 수행할 핸들러를 인자로 받아 생성한다:

const proxyObject = new Proxy(target, handler);

 

핸들러 함수는 트랩(trap)이라고도 부르며, 여러가지 함수가 담긴 객체로 전달한다. 우리는 트랩을 통해 타겟 객체의 값이나 메소드를 바꿔 프록시 객체를 만들게 된다.

 

몇가지 주요 트랩 함수들을 알아보자.

 

handler.get()

타겟 객체의 요소에 접근할 수 있는 함수로, 타겟 객체가 가진 요소의 값을 바꿀 때 쓴다. 타겟 객체(target), 타겟 객체의 요소(property), 그리고 this 값인 receiver가 인자로 전달된다.

 

handler.get() 함수에서 리턴하는 값은 프록시 객체의 모든 요소에 일괄로 들어가버린다. (심지어 타겟 객체에 존재하지 않았던 요소에도 get() 함수가 리턴한 값을 저장하게 된다) 따라서 특정 요소만 바꾸고 싶다면 if문에서 따로 처리해야 된다.

 

또한 프록시 객체를 콘솔에 찍어보면 타겟 객체의 정보만 떠서, 프록시 객체의 트랩 함수가 제대로 동작했는지 확인하려면 바꾼 요소를 직접적으로 확인해봐야 한다.

const student = {
  gpa: 2,
  status: "student"
};

const handler = {
  get(target, prop, receiver) {
    if (prop === "gpa") {
      return 4;
    }
    return "valedictorian";
  },
};

const valedictorian = new Proxy(student, handler);
console.log(valedictorian); // [{ gpa: 2, status: "student" }의 프록시 객체]라고 뜸
console.log(valedictorian.gpa); // 4 - 조건문이 없었다면 "valedictorian"으로 찍힘
console.log(valedictorian.status); // "valedictorian"
console.log(valedictorian.asdf); // "valedictorian" - student 객체에는 없던 요소

 

get() 함수가 리턴하는 값이 프록시 객체의 모든 요소에 들어가게 된다면 그대로 유지했으면 하는 요소까지 영향을 받게 된다. 건드리지 않은 타겟의 요소를 프록시 객체에 그대로 넣어주고 싶으면 Reflect.get() 함수를 리턴하면 되는데, Reflect.get()은 handler.get()과 동일한 프록시 핸들러이기 때문에 handler.get() 함수에 사용한 인자를 받아 사용한다.

 

이미 handler.get() 함수에서 타겟 객체를 일부 변형시키니, Reflect.get() 함수가 타겟 객체의 나머지 부분을 그대로 반환하기 위해 `...arguments`를 인자로 전달한다. 예시로 위 코드에서 Reflect.get() 함수를 쓰도록 수정하면 아래의 결과가 나온다:

const student = {
  gpa: 2,
  status: "student"
};

const handler = {
  get(target, prop, receiver) {
    if (prop === "gpa") {
      return 4;
    }
    return Reflect.get(...arguments); // 함수의 arguments 객체 전달
  },
};

const valedictorian = new Proxy(student, handler);
console.log(valedictorian);
console.log(valedictorian.gpa); // 4
console.log(valedictorian.status); // "student"
console.log(valedictorian.asdf); // undefined

 

이제 타겟 객체에 존재하지 않았던 요소는 프록시 객체에서도 undefined로 뜨는 걸 확인할 수 있다. 추가로 핸들러의 트랩 함수는 선언문으로 작성해야 Reflect.get() 함수에서 arguments 인자를 읽을 수 있다. 함수 표현식을 사용하면 arguments 인자를 참조할 수 없어 reference error가 발생한다.

 

만약 함수 표현식을 쓰려고 한다면 아래처럼 스프레드 문법 대신 handler.get() 함수가 받은 인자와 동일하게 넣어줘야 에러가 발생하지 않는다.

const handler = {
  get: (target, prop, receiver) => {
    if (prop === "gpa") {
      return 4;
    }
    return Reflect.get(target, prop, receiver); // OK
  },
};

 

handler.set()

handler.set() 함수는 프록시 객체의 요소에 대입 연산자를 사용해 다른 값을 저장할 때 실행된다. 프록시 객체에 새로 저장할 값을 검증하는 데에 활용할 수 있다.

 

handler.set()은 타겟 객체(target), 값을 할당하려는 요소(property), 할당할 값(value)을 인자로 받는다.

const student = {
  gpa: 2,
  status: "student"
};

const handler = {
  set(target, prop, value) {
    // gpa 값으로 숫자를 입력했는지 검증
    if (prop === "gpa" && typeof value !== "number") {
      console.log("GPA는 숫자여야 합니다.");
      return;
    }
    return Reflect.set(...arguments);
  },
};

const valedictorian = new Proxy(student, handler);
valedictorian.gpa = "four"; // "GPA는 숫자여야 합니다."
console.log(valedictorian.gpa); // 2

valedictorian.gpa = 4;
console.log(valedictorian.gpa); // 4

 

위 예시처럼 할당할 값이 유용하지 않을 경우 아예 저장하지 못하도록 막을 수 있다.

 

프록시는 언제 쓰이나?

프록시 객체를 사용할 때 유의할 점은 프록시 객체와 타겟 객체는 서로를 참조하고 있어서, 어느 한쪽의 요소 값을 변경하면 다른쪽에도 반영된다는 점이다. 객체에 값을 저장할 때 대입 연산자를 쓸 상황이 온다면 원본을 바꾸거나 깊은 복사를 하지 굳이 프록시를 쓸 필요가 없다. 그렇다면 프록시는 언제 쓸모가 있을까?

Image by Leopictures from Pixabay

 

프록시를 활용하기 좋은 예시 중 하나는 라이브러리의 레거시 코드를 핸들링할 경우다. 라이브러리의 최신 버전에서는 deprecate했지만 이전 버전을 사용하는 유저들에게 backward compatibility를 지원하기 위해 프록시 객체를 만들어, 이전 버전에서 사용되던 요소에 접근했을 때 업데이트된 값을 get하도록 설정하는 것이다.

const legacyFontSizes = {
  large: {
    replacementName: "gigantic",
    replacementValue: "gigantic"
  }
}

const fontSizes = {
  small: "small",
  medium: "medium",
  gigantic: "gigantic"
}

const proxyOptions = {
  get: (target, prop) => {
    if (prop in legacyFontSizes) {
      console.warn("large 요소가 gigantic으로 변경되었습니다.");
      return legacyFontSizes[prop].replacementValue;
    }
    return Reflect.get(target, prop);
  }
}

const proxiedFontSizes = new Proxy(fontSizes, proxyOptions);
console.log(proxiedFontSizes.large); // "gigantic"

 

이외에도 특정 요소에 접근할 수 없도록 하거나 요소가 존재하지 않을 때 다른 값을 반환하도록 하는 등, 필요에 따라 다양하게 활용할 수 있다.

 

 

참고