[자바스크립트] proxy객체의 유연함-기초편(feat, Reflect객체)

Date:     Updated:

카테고리:

태그:

❓Mobx

회사에서 Mobx를 쓴다고 할때는 참 놀랐었다. 이걸 쓰는사람이 있어? 여태까지 염탐해본 프로젝트들은 대부분 Redux를 사용했으며, 가끔 Recoil, 혹은 Vue와 Vuex정도 였기 때문이다.

npmTrends

하지만… 생각보다 많이쓴다!! 그리고 막상 써보니, observable이라는 강력한 기능을 지원한다. 업무에 쓰다보니, 더 효율적인 방법을 계속 생각하다보니, 이제는 redux보다 훨씬 익숙해졌다.


🕵️observable을 관찰하는 observer

스토어의 상태가 변경되면 스토어를 바라보고 있는 친구들은, 상태변경을 감지하고 값을 바꾼다. observable한 객체를 observer들이 바라보고 있는 것이다. 즉, 관찰가능한 store의 값을 잘 관찰하고 있다가, 그 값이 바뀌는순간, 옵저버들도 값을 바꾸는 것이다. 어떻게 값이 바뀌었다는것을 알아차릴 수 있을까?


🕵️앗! 변했다!

어떻게 알 수 있을까? 많은 이들이 알듯, redux를 쓸 때에는 react와 같은 불변성의 원리로 인해 값의 변화를 알아차린다. 메모리 주소값이 변경되는것을 알아차려야 하기 때문이다. 그렇다면 mobx의 변화는 어떻게 알아차릴 수 있는 것일까?

mobx에서는 아래처럼 객체, 배열 내의 값을 바꾸어도 알아서 인지하고 잘 바꿔준다.

a = [1,2,3]
a[2] = 33 //배열 수정도 변화감지 가능(immutable 안달아줘도 됨)

chatGPT에서 본건지, 어느 잘못된 블로그글에서 본건지 나는 이와같은 작동이 deep-compare을 통해서 일어나는 줄 알았다. 객체속의 객체속의 객체속의 객체까지 끝까지 파고들며 모든 값을 비교하는 줄 알았다. 그렇게 동작한다고 믿고 있었다. 하지만 결국 mobx는 proxy객체를 통해 작동한다는 것을 알게 되었다.

오늘의 주인공 프록시다. 프록시가 아래처럼 동네방네 소문을 내주는 것이다.

프록시객체

그렇다면 어떻게 이와같은 구조를 만들 수 있을까? mobx에서는 observable로 객체를 감싸주면 알아서 프록시 객체가 생긴다. 그리고 리액트 컴포넌트들을 observer로 감싸주면 알아서 observable한 객체를 구독하는 구독자들이 되는 것이다. 스타크래프트 옵저버가 몰래 계속 사람들 쳐다보는것처럼, observer들은 계속 본인이 구독하고 있는것을 쳐다보고 있는 것이다.

프록시객체생성


⭐️proxy객체

서론이 길었다. 오늘의 주제이다. 우리는 Mobx에 대해 알아보지 않을 것이고, react나 redux에 대해서는 더이상 설명하지 않을것이다. 자바스크립트의 고오급 기술인 proxy객체에 대해 이야기 해 볼 것이다. 내방 책꽂이에서 잘 쉬고있는, 자바스크립트 중급서의 정석으로 꼽히는 도롱뇽(?)이 그려져있는, 이응모님의 자바스크립트 Deep Dive 에도 나와있지 않은 내용이다. 여러 검색들에도 자바스크립트 “고급” 기술로 분류되어 있다.

도롱뇽

개인적으로 “고급”기술이라 하여, 더 어려운 개념이라고 생각되지는 않는다. 실무에서 “잘 알고” 적절히 활용하기 어려운 정도에 따라 분류된다고 생각한다.

그렇다면 프록시가 뭘까?

프록시는 대신 처리해주는 객체이다

우리가 특정 객체에 대한 작업들을, 대리인을 통해서 하는 것이다. 아래는 프록시 예제에 끊임없이 등장하는 프록시 국룰(?) 예제이다

let obj = {
    name : "김카사디안",
    age : 25
};

let handler = {
  set(obj, prop, value) {
      if (prop === 'age') {
        if (!Number.isInteger(value)) {
          throw new TypeError('숫자로 넣으셈...');
        }
        if (value > 200) {
          throw new RangeError('200살 넘는사람이 어디있음...');
        }
      }
      
      obj[prop] = value; // 값을 저장 (정상작동)
      return true;
    }
};
const person = new Proxy(obj, handler);
person.age = 50;
person.age = "50";
person.age = 300;

위처럼 validation을 검사해 줄 수 있다. 오브젝트의 변경을 프록시라는 대리인? 비서? 객체를 통해 변경하는 것이고 이 때, 핸들러들을 통해 유효성 검사 등을 해줄 수 있는 것이다. mobx와 같은 경우, 값의 변동이 있는 경우 프록시객체를 통해 다른 객체들에게 notice해줄 수 있는 것이다.

정도 까지 알려고 했는데, 같이 공부하는 쥐휴가 날카로운 질문을 했다. “class에서 private로 쓰면 안됨? ㅋㅋ”

생각해보니, 핸들러를 달아주거나, 특정 행동에 대해 trap을 거는 행동들은 class에서 따로 정의를 하는 것으로 충분한데 왜 프록시 객체가 필요한지 떠올리기 힘들었다. 프록시 객체에서 getset 을 통해 할 수 있는 행위들은 class에서의 gettersetter들로 충분히 대응 가능하기 때문이었다. 기타 블로그들에서도 "왜" 프록시를 사용하는지에 대해 명확한 답변을 얻기 어려웠다. 프록시가 사용될 수 있는 다양한 방법들을 살펴보며 왜 프록시가 유용한지에 대해 살펴보도록 하겠다.


⭐️프록시의 짱짱 활용

1_ class보다 더 유연하다

  • 내부에서 유연함 느끼기 setter를 생각해보면 set(obj, prop, value)에서 obj[prop] = value와 같은 방법으로 모든 속성들에 대해서 하나의 setter를 두고 검사할 수 있다. 하지만 class로 작성하게 된다면 모든 속성에 대해 각각 그 메소드들을 따로 정의해주어야 하기 때문에 번거롭고 유연하지 못하다.

  • 외부에서 유연함 느끼기 외부에서 핸들러를 사용할 때도 편하다. 특정 메소드들에 대해 로깅을 남기고 싶은 상황을 떠올려 보자. 클래스를 사용하게 된다면, 모든클래스의 메소드를 찾아다니며 로깅을 남기는 메소드를 추가해야 한다. 하지만 프록시객체에서 사용하게 될 핸들러 하나만 만들어 주면, 그 어떤 객체를 가져와도 동일 핸들러를 붙여주기만 하면 로깅을 남길 수 있다.

정리하자면, setter에서 무언가를 처리해주어야 할 때, 하나의 setter에서 동일 로직을 처리하고(case문을 내부에 둬서 특정 prop에 대한 별도 처리를 하거나) 유연하게 대응할 수 있으며, 하나의 핸들러를 다양한 객체에 적용시킬 수 있는 유연함 등은 class에서 맛보기 힘들기 때문에 프록시를 사용하는 것이 더 적합하다고 볼 수 있다. 이처럼 validation이나 logging을 남기는 상황에 특히 더 유용하기 때문에 proxy로 처리하는 경우가 많다. 물론 기능적으로는, 어떤 방법을 사용해도 동일한 결과를 만들 수 있다.

2_ 디자인 패턴에도 있다.

정~말 자주 쓰이는 디자인 패턴인 프록시 패턴을 구현할 때, 프록시 객체를 사용하면 편하다(당연한 이야기 써봄)

3_ 다양한 활용

다음과 같은 getter와 setter를 위해 사용되는 트랩 핸들러는 익숙할 것이다. 트랩핸들러로 써보지 않았더라도, getter와 setter는 많이 다루어 보았으리라 생각한다.

  • 유명한 트랩: get을 통해서 값을 읽기 set을 통해서 값을 수정

하지만 그 외에도 다양한 트랩들이 있다.

  • 안유명한 트랩: has를 통해 멤버십 테스트를 하는 시점을 가로채기
    apply를 통해 함수를 호출하는 시점을 가로채기
    constructor를 통해 new 연산자 작동 시점 가로채기

그렇다면 두가지 예제를 봐보겠다. 아래 예제에서는 다루지 않았지만, 물론 get과 set이 가장 많이 사용된다.

1번 예제

let obj = {
    name : "김카사디안",
    age : 25
};

let handler = {
  has(target, prop) {
      console.log("프로퍼티 확인 때 중간에 가로채어 로직 시행");
      return true
  },
};

const person = new Proxy(obj, handler);
if("name" in person) {
  console.log("검사잘됨")
}else{
  console.log("검사실패")
}

이처럼 has 트랩을 통해 멤버십 테스트를 하는 로직을 가로 챌 수 있다. 그렇다면 두번째 예제를 보도록 하겠다.

let obj = {
  name: '김카사디안',
  print: function () {
      console.log(`내이름은 ${this.name}`);
  },
};

let handler = {
  apply(target, thisArg, args) {
      console.log('이름 몰래 바꾸기');
      thisArg.name = '김디안';

      Reflect.apply(target, thisArg, args); // 원본 함수 실행시켜주기
  },
};

obj.print(); // 내이름은 김카사디안
obj.print = new Proxy(obj.print, handler);
obj.print(); // 내이름은 김디안

두번째 예제는 apply 트랩을 활용하여 객체 내부의 함수를 실행하는 시점을 intercept 할 수 있었다. 그런데 두번째 예제를 보면 조금 이상한 녀석이 보인다. 바로 Reflect객체를 통한 원본함수 실행이다.

마지막으로 Reflect객체에 대해 알아보며 마무리하도록 하겠다.


Reflect

ReflectProxy처럼 ES6에 추가된 문법이다. Reflect객체에 대해 알기 위해서는 프로그래밍 기법인 Reflection에 대해 알아야 한다. Reflection은 런타임에서 객체의 속성, 변수, 메서드 등을 동적으로 사용하기 위해 쓰이는 기법이다. 기존 자바스크립트에서도 해당 기능들이 있었지만, 너무 구려서 이참에 ES6에서 제대로 만들어 주었다고 한다.

아래의 Reflection 예제를 보고, 위에서 Proxy객체 내부에서 쓰인 Reflect를 보도록 하겠다.

//예제1
const obj = { name: "김카사디안", age: '80'};
Reflect.get(obj, 'age'); //80

//예제2
const add = (a, b) => a + b;
Reflect.apply(add, null, [2, 3]); // 5

생각보다 간단하다. 어떻게 동작하는지도 대충 알 것 같다. 그럼 그냥 add(3,5)를 실행시키면 되는데 왜 Reflect객체를 통해 실행시킬까? 위의 Proxy객체 내부에서 Reflect를 사용했던 예제처럼, Proxy와 Reflect를 같이 사용했을 때의 이점들이 있기 때문이다.

다음글에서 Meta ProgrammingReflective programming에 대해 알아보며, ProxyReflect를 활용한 예제와 그 필요성에 대해 알아보도록 하겠다.


💡마무리

Proxy의 기초적인 사용법을 보며 proxy객체의 유연함에 대해 알아보았다. 그리고 Proxy와 떼낼 수 없는! Reflect의 간단한 사용법에대해 알아보았다. get, set, has, apply등의 모든 트랩들을 동일한 인터페이스로 지원한다. 그렇다. 같이 쓰라고 준 것이다. 다음 글에서는 왜쓰는지, 어디에 쓰는지 애매함 속에 있던 자바스크립트 고급 문법, Proxy와 Reflect에 대해 좀 더 알아보도록 하겠다.


맨 위로 이동하기

javascript 카테고리 내 다른 글 보러가기

댓글 남기기