본문 바로가기

BackEnd

JS OOP 시리즈 1 : 메타 프로그래밍과 Proxy, Reflect 간단하게 알아보기

반응형

해당 글은 여기서도 볼 수 있다 : OOP 시리즈 1 : 메타 프로그래밍과 Proxy, Reflect 간단하게 알아보기 (velog.io)

 

OOP 시리즈 1 : 메타 프로그래밍과 Proxy, Reflect 간단하게 알아보기

해당 게시물은 아래 글들을 참조하여 작성되었다.https://ko.javascript.info/proxyhttps://ui.toast.com/weekly-pick/ko_20210413Proxy는 메타 프로그래밍을 지원하기 위해 나온 기능이다.메타프로그

velog.io

 

해당 게시물은 아래 글들을 요약 정리한 내용이다.
정말 엄청나게 핵심만 뽑아냈으므로 언젠간 둘 다 정독하길 추천한다.

https://ko.javascript.info/proxy
https://ui.toast.com/weekly-pick/ko_20210413

해당 시리즈의 목적

OOP의 최종 종착지라 할 수 있는 IoC와 DI를 자바스크립트/타입스크립트를 통해 이해해본다.
(스포일러 : IoC컨테이너에서 객체를 가져와서 사용한다. 코드에서 명시적 객체 생성(new, 리터럴 객체 생성)을 제거한다)

이를 위해 프록시 > 데코레이터(JS) > 데코레이터(TS) > NestJs, Angular, TypeDI의 의존성 주입을 알아볼 것이다. (사실 마지막 부분은 이전에 작성한 글이 있다.)

메타 프로그래밍

Proxy는 메타 프로그래밍을 지원하기 위해 나온 기능이다.

메타프로그래밍이란 하나의 컴퓨터 프로그램이 다른 프로그램을 데이터로 취급하는 것을 말한다.
어떤 프로그램이 다른 프로그램을 읽고, 분석하거나 변환하도록 설계된 것이다.
메타프로그래밍에 이용되는 언어를 메타 언어라고 하고, 조작 대상이 되는 언어를 대상 언어라고 한다

ex) js to html

메타 언어와 대상 언어가 다를 경우

res.send("<html>Hello world</html>");

ex) js to js

메타 언어와 대상 언어가 같을 경우 - 이를 반영(Reflection)이라 한다.

eval('1 + 1')

Reflection을 이용하면 런타임에 코드 구조(클래스, 모듈, 객체)와 행위(함수와 메소드)를 관리 및 수정(뒤에 알아보자)할 수 있다.

반사형 프로그래밍(Reflective programming)

같은 언어를 이용해 런타임에 같은 언어의 구조와 행위를 관리, 수정하는것이 반영이고,
이를 이용해 프로그래밍 하는 것을 반사형 프로그래밍(Reflective programming)이라 한다.
반영에는 세가지 종류가 있다.

1. Type introspection

런타임에서 프로그램이 자신의 구조에 접근하여 타입이나 속성을 알아내는 능력을 뜻한다.ex) Object.keys()

2. Self-modification

구조를 스스로 변경할 수 있다는 의미로, 예시로 어떤 속성에 접근하기 위해 [] 표기법을 사용하고, delete 연산자로 제거하는 것 등이 있다.

3. Intercession(중재)

언어가 수행되는 일부 의미(메서드, 프로퍼티)를 재정의하는 것을 말한다.
이를 위해 ES2015에서 Proxy가 만들어졌다.
(사실 없어도 가능하지만 Proxy와 Replect가 있으면 구현이 간단하다.)

프록시(대리)

프록시 객체(Proxy object)는 대상 객체(Target object) 대신 사용된다.
대상 객체를 직접 사용하는 대신, 프록시 객체가 사용되며 각 작업을 대상 객체로 전달하고 결과를 다시 코드로 돌려준다.

proxy-image


이러한 방식을 통해 프록시 객체는 JavaScript의 기본적인 명령에 대한 동작을 사용자 정의가 가능하도록 한다. 객체 자체가 처리하는 특정 명령을 재정의할 수 있게 되는 것이다.
이런 명령의 종류는 속성 검색, 접근, 할당, 열거, 함수 호출 등이 대표적이다.
대상 객체를 때로 직접 수정하기도 하며, 자신의 데이터를 사용하기도 한다.

사용 예제 : 배열에 숫자만 넣을 수 있게 하기.

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // to intercept property writing
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // added successfully
numbers.push(2); // added successfully
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError ('set' on proxy returned false)

alert("This line is never reached (error in the line above)");

더 자세한 사용 방법은 https://javascript.info/proxy를 참조한다.

리시버

this로 사용될 객체를 넘기는 파라미터다. 일단 이런게 있다는 것만 알고 넘어가자.

Reflect

Reflect 객체를 사용하면 더 쉽게 프록시를 작성할 수 있다.
Reflect는 Proxy와 같이 JavaScript 명령을 가로챌 수 있는 메서드를 제공하는 내장 객체이다.
Proxy가 트래핑할 수 있는 모든 내부 메서드에 대해 Reflect에는 Proxy 트랩과 동일한 이름과 인수를 가진 해당 메서드가 있다.
따라서 Reflect를 사용하여 작업을 원래 개체로 전달할 수 있다.

Reflect와 리시버

프록시 객체를 프로토타입을 통해 상속한 객체를 사용하면 이상한 일이 벌어진다.

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)

우리가 예상하는 동작은 다음과 같다.

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let admin = {
  __proto__: user,
  _name: "Admin"
};

// Expected: Admin
alert(admin.name); // outputs: Admin

ECMAScript의 스펙은 객체 속성 접근을 다음과 같이 해석한다.

  1. MemberExpression.IdentifireName과 MemberExpression.[Expression] 형태로 해석한다.
  2. 처리 과정을 거쳐 GetValue(V)를 호출하고, 내부 슬롯 메서드인 [[Get]](P, Receiver)을 호출한다.

따라서 위 코드는 다음과 같이 해석된다.

  1. GetValue(V)의 호출에서 P는 탐색하고자 하는 속성명으로 name이고, Receiver는 GetThisValue(V)의 결괏값으로 this 컨텍스트 값 admin이 된다.
  2. [[Get]](P, Receiver)가 호출되면, OrdinaryGet(O, P, Receiver)를 호출한다. O는 admin, P는 name, Receiver는 admin이다.
  3. admin에는 name이 없기 때문에, 프로토타입 체이닝을 통해, user.[[Get]](P, Receiver)를 재귀적으로 호출한다. 여기에서 Receiver(admin)는 그대로 전달된다.
  4. 이후 OrdinaryGet(O, P, Receiver)가 호출되고, O는 user가 된다. 이후 여기에서 name을 찾고 'Admin'를 반환하게 된다.

하지만 프록시를 이용한 동작은 이렇게 해석된다.

  1. admin.name은 없으므로 프로토타입인 userProxy에 가서 찾는다.
  2. userProxy의 get trap이 작동하는데, 여기서 target은 user이므로 user._name이 리턴된다.

여기서 receiver 파라미터가 사용된다. receiver는 현재의 this(admin)을 유지하도록 해준다.
Receiver는 말 그대로 프로토타입 체이닝 속에서, 최초로 작업 요청을 받은 객체가 무엇인지 알 수 있게 해준다.

userProxy를 아래와 같이 바꾼다. 이를 통해 this를 잘 전달할 수 있다..

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
    // return Reflect.get(...arguments); 
  }
});

프록시(와 리플렉션의 보조)가 왜 중요한가?

이후에 Decorator를 공부하며 알아볼 것이다.

반응형