녕후킴

메타프로그래밍이란?

0 views

메타프로그래밍이란?

Proxy와 Reflect에 대한 공부를 하던 도중 메타 프로그래밍이라는 단어가 등장하는데 이해가 안돼서 정리했다.

메타프로그래밍 정의에 앞서서, 메타프로그래밍은 언어 특성이 아니며 별다른 스탠다드도 존재하지 않기 때문에 사용하는 언어와 사람에 따라서 다르게 해석될 수 있음을 전제한다. (여러 문서를 읽어본바 정의가 조금씩은 다르나 큰 틀은 벗어나지 않는 것 같다. 🧐) 그러므로 정의에 관해서 애써 기억할 필요없고, 다만 컨셉에 대해서는 이해해 놓을 필요가 있다.

위키피디아에서는 메타프로그래밍을 다음과 같이 정의한다.

메타프로그래밍은 프로그래밍 기술로, 다른 프로그램을 데이터로 취급하여 분석, 생성, 변형등의 조작을 하는 어떤 프로그램을 작성하는 것

대부분의 문서가 이 정의로 시작을 하는데, 이 정의만을 놓고보면 다음 코드가 왜 메타프로그래밍인지 이해가 잘안간다.

function coerce(value) {
  if (typeof value === 'string') {
    return parseInt(value);
  } else if (typeof value === 'boolean') {
    return value === true ? 1 : 0;
  } else if (value instanceof Employee) {
    return value.salary;
  } else {
    return value;
  }
}

console.log(1 + coerce(true)); // 2
console.log(1 + coerce(3)); // 4
console.log(1 + coerce('20 items')); // 21
console.log(1 + coerce(new Employee('Ross', 100))); // 101

위 코드에서 어떤 프로그램은 무엇이고 다른 프로그램은 무엇일까? 이해를 돕기 위해서 메타프로그래밍을 다시 정의하면 다음과 같이 정의할 수 있다.

메타프로그래밍은 프로그래밍 기술로, 다른 코드를 데이터로 취급하여 분석, 생성, 변형등의 조작을 하는 코드를 작성하는 것을 의미한다. 이는 런타임에 기존 코드가 동작에 맞게 자기 자신을 변형하는 것을 포함한다. 이를 위해서 자바스크립트에서는 Proxy나 Reflect를 이용할 수 있다.

이러한 정의를 바탕으로 앞선 코드를 이해해보면 “corece라는 함수가 들어오는 코드(여기서는 value 인자로, 런타임에는 유저가 입력한 무언가가 될수 있을 것 같다.)에 따라서 typeof를 통해서 분석한 뒤 알맞은 동작을 수행하고, 기존 코드(log되는 결과)가 변형될 수 있겠구나”정도로 이해할 수 있을 것 같다.

참고로 이 문서를 다른 분께 공유드리면서 “메타”의 정의에 대해서 말씀해 주셨는데, 쉽게 말해서 “A에 대한 A”라고 이해하면 된다. 실제로 메타 데이터 용어를 위키피디아에서는 다음과 같이 정의하고 있다.

메타데이터(metadata)는 데이터(data)에 대한 데이터이다.

다시 메타프로그래밍으로 돌아와서, 메타프로그래밍은 크게 다음 두가지 능력을 갖고 있다.

  1. 프로그램 코드를 생성하는 능력(Code Generation)
  2. 프로그램이 자기 자신을 조작하거나 다른 프로그램을 조작할 수 있는 능력(Reflection 혹은 Reflective Programming)

그리고 Reflection은 다시 다음 세가지로 분류할 수 있다.

  1. introspection(분석)
  2. intercession(중재)
  3. self-modification(자기 수정)

각각에 대해서 알아보자.

우선 코드를 생성하는 코드로는 eval을 예로들수 있다. string으로 작성된 자바스크립트 코드는 런타임에 실제 자바스크립트 코드가 생성되어 실행된다.

eval(`
  function sayHello() {
    console.log("Hello World");
  }
`);

// sayHello라는 함수가 이미 정의돼 있는 것 처럼 호출이 가능하다.
sayHello();

그리고 분석(introspection)과 관련한 코드로는 ES6 이전에는 typeof, instanceof, Object.* 등을 이용할 수 있고, ES6 이후부터는 introspection을 위한 Reflect API가 도입되었다. 다음 코드에서 instanceof는 특정 함수의 인스턴스인지 확인함으로써 introspection을 수행하고있다.

function Pet(name) {
  this.name = name;
}

const pet = new Pet('Bubbles');

console.log(pet instanceof Pet);
console.log(pet instanceof Object);

조정(intercession)은 기본 동작을 재정의하는 것이다. 원본(target)을 수정하지 말아야 한다는 전제가 존재한다. ES6부터 Proxy를 이용해서 가능하며, ES5에서는 getter와 setter를 이용해서 비슷하게 구현 가능하지만, 원본이 수정된다는 점에서 intercession으로 보기 어렵다.

var target = { name: 'Ross', salary: 200 };

var targetWithProxy = new Proxy(target, {
  get: function (target, prop) {
    return prop === 'salary' ? target[prop] + 100 : null;
  },
});

console.log('proxy:', targetWithProxy.salary); // proxy: 300
console.log('target:', target.salary); // target: 200

Proxy는 두번째 인자에 정의된 핸들러 객체를 전달할 수 있다. 핸들러 객체 내부에는 동작을 가로채는 get과 set과 같은 trap이 정의될 수 있다. targetWithProxy.salary에 접근할 때, trap 함수인 get 함수가 기존 프로퍼티에 + 100을 더하여 읽기 동작이 수행되도록 읽기 동작을 재정의하고 있다.

self-modification은 프로그램이 자기 자신을 수정할 수 있는 것이다. intercession과는 다르게 원본이 변경된다.

var blog = {
  name: 'freeCodeCamp',
  modifySelf: function (key, value) {
    blog[key] = value;
  },
};

blog.modifySelf('author', 'Tapas');

여기까지 메타프로그래밍에 대해서 알아보았다. 다시 한 번 언급하지만 메타프로그래밍은 “프로그래밍 언어 특징”이나 “표준화된 것”으로 묘사될 수 없고, “수용력(Capacity)“에 가깝다. Go와 같은 몇몇 프로그래밍 언어는 메타프로그래밍을 완전히 지원하지 않고 일부만 지원한다.

📚 참고문헌

A brief introduction to Metaprogramming in JavaScript

Metaprograaming with Proxies

Comprehensive Guide To Metaprogramming in Javascript

Exploring Metaprogramming, Proxying And Reflection In JavaScript


Reflect API는 왜 도입됐을까?

ES6에 도입된 Reflect는 introspection을 위한 메서드들을 제공한다. 하지만 이는 ES5에 이미 Object와 Function 객체에 존재했던 메서드들이다. 이미 메서드들이 존재하는데 Reflect API를 도입한 이유가 뭘까? 그 이유는 다음과 같다.

1. All in one namespace

ES6 이전에는 Reflection과 관련한 기능들이 하나의 네임스페이스 안에 존재하지 않았다. ES6부터는 Reflection과 관련한 기능들이 Reflect API 내에 존재하게된다. 또한 Object 처럼 생성자로 호출이 불가능하고, 함수로의 호출이 불가능(non-callable)하며, 메서드들은 모두 정적 메서드들이다. 우리는 연산을 위해서 흔히 Math 객체를 사용하는데, Math 객체 역시 생성자로 호출이 불가능하고, 함수로의 호출이 불가능하며, 메서드들이 모두 정적 메서드들이다.

2. Simple to use

사용하기가 쉽다. Object 객체에 존재하는 introspection과 관련한 메서드들은 동작이 실패하는 경우 예외를 발생시킨다. 개발자 입장에서는 예외를 처리하기보다는 Boolean 결과를 처리하는게 편하다. 예를들면 Object에 존재하는 defineProperty는 다음과 같이 사용해야 한다.

try {
  Object.defineProperty(obj, name, desc);
} catch(e) {
  // handle the exceptionl
}

하지만 Reflect API를 사용하는 경우, 다음과 같이 사용이 가능해진다.

if(Reflect.defineProperty(obj, name, desc)) {
  // success
} else {
  // failure
}

3. 신뢰성 있는 apply() 메서드의 사용

ES5에서 함수를 this value와 함께 호출하기 위해서 보통 다음과 같이 사용했다.

Function.prototype.apply.call(func, obj, arr);
// or
func.apply(obj, arr);

하지만 이러한 접근은 func 함수 내에 apply라는 메서드가 존재하는 경우가 있을 수 있기 때문에 신뢰성이 떨어진다. Reflect는 apply 메서드를 제공함으로써 이러한 문제를 해결한다.

Reflect.apply(func, obj, arr);

📚 참고문헌

What is Metaprogramming in JavaScript? In English, please