ES6 In Depth 는 ECMAScript 표준의 6번째 버전(이하 ES6) 에서 JavaScript 프로그래밍언어에 추가된 새로운 기능들에 대한 연속기획물 입니다.

여기에 오늘 우리가 살펴볼 기능이 있습니다.

var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});

첫 예제로는 약간 복잡하죠. 나중에 모두 설명할 것입니다. 지금은 우리가 만든 객체만 살펴보도록 하죠.

> obj.count = 1;
setting count!
> ++obj.count;
getting count!
setting count!
2

무슨 일이 일어난 걸까요? 우리는 이 객체의 property 접근을 가로채고(intercept) 있습니다. 우리는 “.” 연산자를 overload 하고 있습니다.

How it’s done

컴퓨팅에서(in computings) 최고의 속임수(trick)는 가상화(virtualization) 라고 불립니다. 그것은 놀라운 일을 하기 위한 매우 범용적인 기술입니다. 아래에서 동작방식을 설명합니다.

  1. Take any picture. 어떤 사진을 찍습니다.

Photo credit: Martin Nikolaj Bech

2. 사진에서 특정 부분의 주변으로 윤곽(outline)을 그립니다.

3. 지금 outline 안의 모든것 또는 outline 바깥의 모든 부분을 완전히 예상치 못한 것으로 대체했습니다. 한가지 룰이 있다면, 이전 버전과의 호환성입니다. 당신의 대체물은 그곳에 원래 무엇이 있었는지 선의 반대편에 어떤것이 바뀌었는지 아무도 알아차리지 못해야 합니다.

Photo credit: Beverley Goodwin.

당신은, 인간이 outline의 안에 있고 세상의 나머지 부분이 정교한 환상으로 대체된 Truman Show 와 The Matrix 와 같은 고전 컴퓨터 과학 영화에서 본 이런 종류의 hack 을 잘 알고 있을 것입니다.

이전 버전과의 호환성을 만족시키기 위하여, 당신의 대체물은 정교하게 설계되어야 할 필요가 있을 겁니다. 그러나 진짜 트릭은 올바른(right) 아웃라인을 그리는 것에 있습니다.

outline 에 의해서, 저는 API boundary 를 표현합니다. 인터페이스입니다. 인터페이스는 어떻게 코드의 두 부분이 상호작용하고 각 부분들이 다른부분에 무엇을 기대하는 지를 구체화합니다. 그래서 만약 시스템에 인터페이스가 설계되어 있으면, 당신을 위한 outline 은 이미 그려져 있는 것입니다. 당신은 어떤부분을 대체 할 수 있는지를 알고, 그 밖의 부분에는 신경쓸 필요가 없습니다.

존재하는 인터페이스가 없을때, 당신은 창조적이어야만 합니다. 가장 멋진 소프트웨어 hack 의 일부는 이전에는 없었던 곳에 API 경계를 그리는 것을 항상 포함하고, 놀라운 엔지니어링 노력을 거쳐 인터페이스를 생겨나게 합니다.

Virtual memory, Hardware virtualization, Docker, Valgrind, rr — 이런 다양한 프로젝트들은 새로 운영하고 이미 존재하는 시스템의 예상치 못한 인터페이스들과 연관되어 있습니다. 몇몇 경우, 잘 동작하는 새로운 경계를 만들기 위해서 몇년의 시간과 새로운 OS 기능들과 심지어 새로운 하드웨어가 필요했습니다.

최고의 가상화 hacks 은 그들에게 가상화되어질 것들에 대한 새로운 이해(understanding)를 가져옵니다. 어떤 것을 위한 API를 쓰기위해서는 그것을 이해해야만 합니다. 이해한 후, 당신은 놀라운 것들을 할 수 있습니다.

ES6 는 자바스크립트의 가장 근본적인 개념(concept)을 위한 가상화 지원기능을 도입했습니다. : the object

What is an object?

잠시만요. 생각해봅시다. 객체가 무엇인지 생각해본 후에 스크롤을 내려주세요.

Photo credit: Joe deSousa.

이 질문은 제겐 너무 어렵군요! 저는 정말 만족할 만한 정의를 들어본적이 없습니다.

놀라운가요? 근본적인 개념 정의는 항상 어렵습니다 — Euclid’s Elements 에서 첫 몇가지 정의를 확인해보세요. ECMAScript 언어 명세도 마찬가지로, “Object 타입의 멤버” 라며 객체 정의에 별 도움이 되지 않고 있습니다.

추후에, 명세는 “object 는 properties 의 collection” 이라는 문구를 추가했습니다. 나쁘진 않군요. 만약 당신이 정의를 원한다면, 지금 하면 됩니다. 일단은 나중에 생각해보도록 하죠.

저는 앞에서 어떤것에 대한 API 를 작성하기 위해 당신은 그것을 이해해야만 한다 고 말했습니다. 만약 우리가 객체를 더 잘 이해한다면, 놀라운 것들을 할 수 있을 것이라고 약속합니다.

그러면 JavaScript ECMAScript 표준 위원회의 발자취를 따라 JavaScript objects 를 위한 API 와 인터페이스에 무엇이 정의되었는지 보도록 합시다. 우리가 필요로 하는 메서드는 무엇일까요? objects 는 무엇을 할 수 있을까요?

객체마다 다소 다릅니다. DOM Element objects 는 특정한 것을 할 수 있습니다; AudioNode objects 는 다른것들을 할 수 있습니다. 그러나 모든 객체가 공유하는 근본적인 능력이 몇가지 있습니다:

  • objects 는 properties 를 가집니다. 당신은 properties 를 설정하고 읽어오고, 삭제하는 등의 것을 할 수 있습니다.
  • objects 는 prototype 을 가집니다. 이것은 JS 에서 상속이 작동하도록 하는 방법입니다.
  • 몇가지 objects 는 함수(function) 또는 생성자(constructor) 입니다. 당신은 그것들을 호출할 수 있습니다.

object 와 함께하는 거의 모든 JS 프로그램은 properties, prototypes 그리고 functions 를 사용하고 있습니다. Element 또는 AudioNode object 의 공통 동작은 상속된 function properties 인 methods 호출에 의해서 접근된다는 것입니다.

그래서 ECMAScript 표준 위원회가 모든 objects 를 위한 공통 인터페이스로 14가지 내부 메서드들을 정의했을때, 이러한 세가지 근본적인 것에 초점을 맞춰 마무리 한것이 특별하진 않았습니다.

전체 목록은 tables 5 and 6 of the ES6 standard 에서 확인할 수 있습니다. 여기 일부를 나열하겠습니다. 이중 대괄호, [[ ]], 는 이것이 JS code의 숨겨진 내부(internal) 메서드인 것을 강조한 것입니다. 이런 메서드는 호출하거나 삭제하거나 덮어씌울 수 없습니다.

obj.[[Get]](key, receiver) — 프로퍼티의 값을 가져옵니다.

JS code에서 호출하는 법: obj.prop 또는 obj[key].

obj 는 검색대상이 되는 객체입니다; receiver 는 우리가 this 프로퍼티를 처음 검색하기 시작한 객체입니다. 때때로 우리는 여러 객체들을 검색해야 해야만 합니다. obj 는 receiver 의 프로토타입 체인 위의 객체일 지도 모릅니다.

obj.[[Set]](key, value, receiver) — 객체에 프로퍼티를 할당합니다.

JS code에서 호출하는 법: obj.prop = value 또는 obj[key] = value.

obj.prop += 2 처럼 할당하면, [[Get]] 메서드가 먼저 호출되고, [[Set]] 메서드가 곧이어 호출됩니다. ++ 와 — 도 마찬가지입니다.

obj.[[HasProperty]](key) — 프로퍼티가 존재하는지 아닌지 확인합니다.

JS code에서 호출하는 법: key in obj

obj.[[Enumerate]]() — obj의 나열가능한 프로퍼티를 나열합니다.

JS code 에서 호출하는 법: for (key in obj) …

이것은 iterator 객체를 반환하고, 객체의 프로퍼티 이름들을 for-in 반복문을 통해 가져오는 방법입니다.

obj.[[GetPrototypeOf]]() — obj 의 프로토타입을 반환합니다.

JS 코드에서 호출하는 법: obj.__proto__ 또는 Object.getPrototypeOf(obj).

functionObj.[[Call]](thisValue, arguments) — Call a function. 함수를 호출합니다.

JS 코드에서호출하는 법: functionObj() 또는 x.method().

Optional. 모든 객체가 함수는 아닙니다.

constructorObj.[[Construct]](arguments, newTarget) — 생성자를 호출합니다.

JS code 에서 호출하는 법: 예를들면, new Data(2890, 6, 2)

Optional. 모든 객체가 생성자는 아닙니다.

newTarget 매개변수는 subclassing 의 역할을 합니다. 나중에 설명하도록 하겠습니다.

아마도 당신은 다른 일곱가지중의 일부를 추측할 수 있을 것입니다.

ES6 표준에 의하면, 가능하면, 객체의 문법 또는 내장함수의 일부는 14가지 내장 메서드에 기초를 두고 서술되었습니다. ES6는 object 의 주위에 명확한 경계를 그렸습니다. proxies는 임의의 JS code와 함께 표준의 핵심부분을 대체할 수 있게 해줍니다.

기억해두세요 우리가 곧 이런 내부 메서드를 덮어쓰는 것에 대해서 이야기하기 시작할때, 우리는 Object.keys() 와 그 밖의 내장 함수, 그리고 obj.prop 와 같은 핵심 문법의 동작을 덮어쓰는 것에 대해서 이야기 할 것입니다.

Proxy

ES6는 target 객체와 handler 객체, 두개의 매개변수를 가지는 새로운 전역 생성자 Proxy 를 정의합니다. 아래에 간단한 예제처럼요:

var target = {}, handler = {};
var proxy = new Proxy(target, handler);

잠시 handler 객체를 무시하고 어떻게 proxy 와 target 이 연결되는지에 집중해봅시다.

저는 한문장으로 어떻게 proxy 가 동작하는지 말할 수 있습니다. 모든 proxy 의 내부 메서드는 target 에 연결됩니다. 즉 만약 proxy.[[Enumerate]]() 를 호출하면 바로 target.[[Enumerate]]() 가 반환될 것입니다.

시도해봅시다. proxy.[[Set]]() 가 호출되도록 해봅시다.

proxy.color = "pink";

좋아요 무슨일이 일어났나요? proxy.[[Set]]() 는 target.[[Set]]() 을 호출해서, target 의 새로운 property 를 만들었어야만 합니다. 그렇게 되었나요?

> target.color
"pink"

그렇습니다. 모든 다른 내부 메서드도 마찬가지입니다. 모든 부분에서 이 proxy 는 정확히 target 과 같은 동작을 수행할 것입니다.

한계가 있습니다. 당신은 proxy !== target 인 것을 알 수 있을것입니다. 그리고 proxy 는 때때로 target 은 pass 되는 type checks 에 실패할 것입니다. 예를 들어 비록 proxy’s target 이 DOM Element 일지라도, proxy 는 정말 Element 가 아닙니다; 그래서 document.body.appendChild(proxy) 와 같은 것들은 TypeError 와 함께 실패할 것입니다.

Proxy handlers

이제 handler 객체로 돌아와봅시다. 이것이 proxies 를 유용하게 만들어줍니다.

handler 객체의 메서드들은 proxy 의 내부 메서드를 모두 덮어쓸 수 있습니다.

예를 들면, 만약 당신이 object’s properties 를 할당하기 위한 모든 시도를 가로채고싶다면, 당신은 handler.set() 메서드를 정의함으로써 할 수 있습니다:

var target = {};
var handler = {
set: function (target, key, value, receiver) {
throw new Error("Please don't set properties on this object.");
}
};
var proxy = new Proxy(target, handler);
> proxy.name = "angelina";
Error: Please don't set properties on this object.

handler 메서드의 모든 리스트는 Proxy 를 위한 MDN 페이지에 문서화 되어있습니다. 14개의 메서드가 있으며, ES6에서 정의된 14 내부 메서드와 같이 나열되어 있습니다.

모든 handler 메서드는 선택적(optional)입니다. 만약 내부 메서드가 handler에 의해 가로채지지(intercepted) 않았다면, 우리가 이전에 본 것처럼 target 으로 전달됩니다.

Example: “Impossible” auto-populating objects

우리는 이제, 신기하고 프록시없이 불가능한 것들에 대해 시도해 볼만큼 충분히 알게 되었습니다.

여기 첫번째 실전이 있습니다. 이렇게 할 수 있는 Tree() 함수를 만들어보세요:

> var tree = Tree();
> tree
{ }
> tree.branch1.branch2.twig = "green";
> tree
{ branch1: { branch2: { twig: "green" } } }
> tree.branch1.branch3.twig = "yellow";
{ branch1: { branch2: { twig: "green" },
branch3: { twig: "yellow" }}}

branch1, branch2 그리고 branch3 의 중간 객체들이 필요할때 마법처럼 자동으로 생성되는 부분에 주목하세요. 편리하네요 그렇지 않나요? 어떻게 이런것들이 가능한걸까요?

지금까지, 그렇게 할 수 있는 방법은 없었습니다. 그러나 proxies 와 함께 약간의 코드만 있으면 됩니다. 우리는 바로 tree.[[Get]]() 을 살펴 볼 필요가 있습니다. 만약 당신이 도전하는 것을 좋아한다면, 읽기전에 스스로 이것을 구현해 봐도 좋습니다.

Not the right way to tap into a tree in JS. Photo credit: Chiot’s Run.

여기 제 해법이 있습니다:

function Tree() {
return new Proxy({}, handler);
}
var handler = {
get: function (target, key, receiver) {
if (!(key in target)) {
target[key] = Tree(); // auto-create a sub-Tree
}
return Reflect.get(target, key, receiver);
}
};

마지막에 Reflect.get()을 호출한 부분에 주목하세요. proxy handler method 에서, “지금 target 에 위임의 기본 동작을 해” 라는 공통적인 요구사항이 필요하게 되었습니다. 그래서 ES6 는 정확히 그러한 동작을 하기위해 사용할 수 있는 14개의 메서드와 함께 새로운 Reflect object를 정의합니다

Example: A read-only view

proxies 는 사용하기 쉽다는 잘못된 인상을 주었을지도 모른다고 생각합니다. 만약 그렇다면 좀 더 예제를 보도록 합시다.

이번 할당은 좀 더 복잡합니다: 우리는 object 를 받아서 그것을 변화시키는(mutate) 능력이 없다는 것을 제외하고는 그 object 처럼 동작하는 proxy 를 반환하는 readOnlyView(object) 라는 하나의 함수를 구현해야만 합니다. 예를 들면, 이렇게 동작해야만 합니다:

> var newMath = readOnlyView(Math);
> newMath.min(54, 40);
40
> newMath.max = Math.min;
Error: can't modify read-only view
> delete newMath.sin;
Error: can't modify read-only view

이것을 어떻게 구현해야 할까요?

첫번째 스텝은 그 target 객체를 수정하는 모든 내부 메서드를 가로채는 것입니다. 다섯가지가 있습니다.

function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// Override all five mutating methods.
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}

잘 동작합니다. read-only view를 통해서 할당하고 property를 정의하는 것들을 막습니다.

이 설계에 구멍이 있나요?

가장 큰 문제는 [[Get]] 메서드와 그 외의 것들이 여전히 변경할 수 있는 객체를 반환할지도 모른다는 것입니다. 그래서 비록 x 라는 어떤 객체는 read-only view 일지라도, x.prop 는 변경할 수 있을지(mutable) 모릅니다! 이건 큰 구멍입니다.

그것을 막기위해, 우리는 handler.get() 메서드를 추가해야만 합니다:

var handler = {
...
  // Wrap other results in read-only views.
get: function (target, key, receiver) {
// Start by just doing the default behavior.
var result = Reflect.get(target, key, receiver);
    // Make sure not to return a mutable object!
if (Object(result) === result) {
// result is an object.
return readOnlyView(result);
}
// result is a primitive, so already immutable.
return result;
},
  ...
};

이것 하나로는 충분하지 않습니다. getPrototypeOf 와 getOwnPropertyDescriptor 를 포함한 다른 메서드들을 위해 유사한 코드가 필요합니다.

그리고 몇가지 문제가 더 있습니다. 이런 proxy를 통해 getter 또는 메서드가 호출될때, getter 나 method 에서의 전달된 this 값은 보통 proxy 가 될 것입니다. 그러나 앞에서 봤듯이, 많은 접근자(accessor) 와 메서드는 proxy 가 통과하지 못할 type check 를 수행합니다. 여기서 proxy 보다는 target object 로 대체하는 것이 더 낫습니다. 어떻게 해야할지 알 수 있겠나요?

이런 형태 설계의 교훈은 proxy 를 만드는 것은 쉽지만, 직관적으로(intuitive) 동작하는 proxy 를 만드는 것은 매우 어렵다는 것입니다.

Odds and ends

What are proxies really good for? (프록시가 정말 좋은가요?)

당신이 객체를 관찰하거나 로깅을 원할때는 정말 유용합니다. 디버깅을 위해서도 편리할 것입니다. 테스팅 프레임웍이 mock objects 를 만들기 위해 그것을 사용할 수도 있습니다.

만약 당신이 일반적인 객체가 하는 동작의 바로 이전 작업을 필요로 할때 proxies 는 유용합니다.

저는 이런 작업들을 싫어하지만, proxies 를 사용하는 코드안에서 무슨일이 일어나는지 알기 위한 가장 좋은 방법의 하나는… proxy의 handler 객체를 또다른 proxy 안에서 모든 handler 메서드가 접근되는 모든 경우를 console 에 로깅하는 것입니다.

우리가 본 readOnlyView 처럼 Proxies 는 객체의 접근을 제한하기 위해 사용될 수도 있습니다. 그런 사용 케이스는 application code 에서는 드뭅니다, 그러나 Firefox 는 다른 domain 사이의 security boundaries 를 구현하기 위해 내부적으로 proxies 를 사용합니다. 그것들은 우리 security model 의 중요한 부분입니다.

Proxies ♥ WeakMaps. readOnlyView 예제에서 우리는 객체에 항상(every time) 접근하는 새로운 proxy 를 만들었습니다. 우리가 WeakMap 에 만든 모든 proxy 를 cache 하는 것은 많은 메모리를 아껴서 객체가 readOnlyView에 전달될 때마다, 오직 그것을 위한 한개의 proxy 가 만들어집니다.

이것은 WeakMap 의 흥미로운 케이스중의 하나입니다.

Revocable proxies. ES6 는 또한 나중에 취소(revoked) 될 수 있음을 제외하고는 new Proxy(target, handler) 와 같은 proxy 를 만드는 또 다른 함수 Proxy.revocable(target, handler) 를 정의합니다. (Proxy.revocable 은 .proxy property 와 .revoke 메서드를 가진 객체를 반환합니다.) proxy 가 취소되면, 단순히 더 이상 아무것도 하지 않습니다; 그것의 모든 내부 메서드는 예외를 던집니다.

Object invariants. 특정 상황에서, ES6는 target object 의 상태(state) 와 일치하는 결과를 기록(report)하기 위한 proxy handler method 를 필요로 합니다. 모든 객체, 심지어 proxies 에 걸쳐 불변성에 대한 규칙을 강제하기 위해 이런 것을 합니다. 예를 들어, 만약 그 target 이 정말 확장될 수 없는게 아니라면, proxy 는 확장될 수 없도록 주장할 수 없습니다.

정확한 규칙(rule)은 여기서 논하기에 매우 복잡하지만, 만약 당신이 “proxy can’t report a nonexistent property as non-configurable” 이라는 error 메세지를 봤다면 이것이 원인입니다. 가장 괜찮은 해법은 proxy 가 그 스스로에 대해 기록하도록 변경하는 것입니다. 또 다른 가능성은 proxy 가 reporting 하는 것이 무엇이든 반영하기위해 즉시 target 을 변경하는 것입니다.

What is an object now?

위에서 우리가 남겨둔 문제를 생각해봅시다: “객체(object)는 속성(properties) 의 모음(collection) 입니다.”

저는 이런 정의가 만족스럽지 않습니다, 게다가 프로토타입과 가능성을 보면 당연하다고 생각합니다. 저는 proxy 가 할 수 있는 것을 얼마나 거지같이(poorly) 정의했는지를 감안하면 “collection” 이란 단어가 너무 넓은 의미라고 생각합니다. proxy의 handler method 는 어떤것이든 할 수 있습니다. 그것들은 random results 를 반환할 수도 있습니다.

object 가 할 수 있는 것을 이해하고, method 를 표준화하고, 모두가 사용할 수 있는 일급 객체 기능으로서 가상화를 추가하여, ECMAScript 표준 위원회는 가능성의 영역을 확장해왔습니다.

객체는 이제 거의 어떤것이든 될 수 있습니다.

“객체가 뭐지?” 라는 질문에 대한 아마도 가장 정직한 대답은 정의로서의(as a definition) 12가지 필수 내부 메서드를 가진다는 것 일 겁니다. 하나의 객체(An object)는 JS 프로그램안에서 하나의 [[Get]] 명령어(operation), 하나의 [[Set]] 명령 등등을 가진 것(something) 입니다.

우리는 object 를 더 잘 이해하게 되었나요? 확신할 수는 없군요! 우리가 놀라운 것을 했나요? 그렇습니다. 우리는 이전의 JS 에서는 결코 가능하지 않았단 것들을 했습니다.

지금 바로 Proxies를 사용할 수 있나요?

아니요! 어쨌든 Web 에서는 안됩니다. 오직 Firefox 와 Microsoft Edge 에서 polyfill 없이 proxies 를 지원합니다.

V8 이 Proxy 명세의 예전 버전을 구현했기 때문에, Node.js 나 io.js 에서 proxies 를 사용하기 위해서는 기본적으로 꺼져있는 옵션( — harmony_proxies) 과 harmony-reflect 폴리필이 필요합니다. (이 문서의 이전 버전은 이부분에 대해 옳지 않은 정보를 포함하고 있었습니다. 댓글로 실수를 고쳐주신 Mörre and Aaron Powell 에게 감사를 표합니다.)

proxies 와 함께 자유롭게 실험해보세요! 지금이 시기입니다.

Proxies 는 Andreas Gal에 의해 2010년에 처음 구현되었고 Blake Kaplan 에 의해 코드리뷰 되었습니다. 표준 위원회는 완전하게 기능을 재설계 했습니다. Eddy Bruel 가2012 년에 새로운 명세로 구현했습니다.

저는 Jeff Walden의 코드리뷰와 함께 Reflect 를 구현했습니다. 그것은 이번주 Firefox Nightly 에 있을 것입니다 — 아직 구현하지 않은 reflect.enumerate()를 제외하고.

다음에, 우리는 ES6에서 가장 논란이 되는 기능에 대해 이야기해 볼 것입니다. 다음주에 Mozilla engineer Eric Faust 와 현재의 ES6 classes 를 함께하시죠.


발번역은 죄송 ;)

This work is licensed under the Creative Commons 저작자표시-동일조건변경허락 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.