es6for3 시리즈
ES6는 새로운 언어철학을 제시하고 편리한 기능을 제공합니다. 하지만 크롬(52버전까지)조차 별도로 플래그를 활성화해야 만 쓸 수 있는 상황입니다.
이에 업계는 바벨 등의 번역기를 이용하여 ES5로 번역시키는 수를 사용하고 있습니다. 결국 바벨이 번역한 코드는 바벨라이브러리를 사용하는 ES5코드로 번역됩니다. ES6에 대한 깊은 이해는 ES6의 코드 방식을 하위 ES3.1로 번역하는걸 가능하게 하죠. ES6 기능에 대한 정확한 이해와 나만의 바벨을 만들기 위한 첫 단계로 ES6의 각 기능을 ES3.1로 번역해보죠.
심볼을 대체할 수 있는가?
결론적으로 없습니다. 하지만 심볼의 의미와 역할을 대신하는 정도는 만들어볼 수 있을 것 같네요. 사실 심볼의 탄생이유이기도 한데 심볼에 대한 의미는 모질라 블로그에 친절하게 설명하고 있습니다.
http://hacks.mozilla.or.kr/2015/09/es6-in-depth-symbols/
이 포스트의 중간쯤에 다음과 같은 코드가 나옵니다.
// 1024 유니코드 난수 문자열 얻기 var isMoving = SecureRandom.generateName(); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true;
그리고 여기에 대해 다음과 같이 설명합니다.
object[name] 문법은 글자 그대로 모든 문자열을 속성(property) name으로 사용할 수 있게 합니다. 그래서 이 방법은 통할 것입니다. 이름 중복은 실질적으로 불가능하니다. 그리고 이 코드는 보기에 괜찮습니다. 하지만 이 방법은 디버깅 하기가 아주 나쁩니다. 해당 속성을 갖는 엘리먼트를 console.log() 코드로 찍을 때마다, 당신은 아주 긴 난수 문자열을 보게 될 것입니다.
이것에 대한 대안이 바로 심볼입니다만, 심볼이 제공되지 않은 언어에서 취할 수 있는 최선의 선택은 바로 위의 코드라는 것이죠. 하지만 디버깅이 힘든 건 사실입니다. 따라서 바로 이 점에 초점을 맞춰서 조금 더 개선한 버전을 만들어보죠.
toString을 이용하기
결국 객체의 키로 사용될 때는 obj[key] 형태로 사용될텐데, es5까지에서 key가 문자열이 아니면 내부적으로는 자동으로 toString을 호출하게 됩니다. 예를 들자면 다음과 같죠.
var key = { toString:function(){ return 'test'; } }; var obj = {}; obj[key] = 3; console.log(obj.test == 3); //true
대괄호에 문자열이 아닌 무언가가 들어오면 toString을 이용해 문자열로 바뀐다는 사실을 이용하면 외부에서는 객체로 인식되고 내부에서는 문자열로 처리되는 중간단계의 객체를 만들어볼 수 있을 것입니다.
심볼의 기본조건은 유일한 키를 만들어내는 것이므로 문자열이 유일하도록 만들어내는 함수가 필요할 것입니다. 모질라에 나왔던 SecureRandom.generateName() 따위의 알 수 없는거 말고 실질적으로 써 먹을만 한 uuid 함수를 하나 만들어보죠. 우선 uuid에 대한 표준으로 RFC 4122가 있습니다.
여기 나와있는 스펙대로 구현해보면 다음과 같은 코드가 됩니다.
var uuid = (function(){ var seed = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; var reg = /[xy]/g; var f = function(str){ var r = Math.random()*16|0; var v = str == 'x' ? r : (r&0x3|0x8); return v.toString(16); }; return function(){ return seed.replace(reg, f); }; })(); console.log(uuid()); //3bce4931-6c75-41ab-afe0-2ec108a30860
나쁘지 않고 표준이기도 합니다. 하지만 상당한 양의 Math.random()을 불러야합니다. Math.random()은 Math의 모든 연산 중에 가장 무거운 연산입니다. 해서 어느 정도의 타협점을 갖는 편이 좋을 수 있습니다.
var uuid = (function(){ var uuid = 0; return function(){ return '@@Symbol:' + (+new Date) + ':' + (uuid++) + ':' + (Math.random() + '').substr(0, 5); }; })(); console.log(uuid()); //@Symbol:1470744002330:10:0.795_
getTime하에서 uuid의 단조증가값과 합해 살짝 랜덤키를 뿌리는 정도에서 충분하지 않나 싶습니다. 해서 실 구현에서는 두 번째 랜덤함수를 사용하도록 하겠습니다.
이제 재료가 모였으니 간단한 심볼클래스를 만들어볼 수 있겠네요. Symbol은 구현된 브라우저에서는 예약어이니 Symb 정도로 이름지어보겠습니다.
var Symb = (function(){ var Symb = function(key){ this.uuid = uuid(); this.key = key || ''; }; Symb.prototype.toString = function(){ return this.key + ':' + this.uuid; }; return function S(key){ if(this instanceof S) throw 'call only'; return new Symb(key); }; })(); var a = Symb(); var obj = {}; obj[a] = 3; console.log(obj[a]); //3
본디 심볼은 new하면 예외가 발생하기 때문에 그것을 추가한 형태의 팩토리함수를 노출하도록 처리했습니다. 또한 심볼생성시 키를 넘길 수 있지만 키가 같다고 같은 심볼은 아니기 때문에 매번 new Symb를 해서 반환하고 있습니다.
정적 메소드의 구현
심볼에는 중요한 두 가지 정적 메소드가 있습니다.
- Symbol.for(key) – key로 식별할 수 있는 심볼을 만든다.
- Symbol.keyFor(symbol) – Symbol.for로 만들어진 심볼의 키를 얻는다.
그냥 Symbol(‘aaa’) 라고 할 때의 ‘aaa’ 아무것도 아닌 값이지만 Symbol.for로 만들면 전역에서 키로 식별되는 유일한 심볼을 만들어냅니다. 간단히 코드로 표현하면 다음과 같습니다.
console.log(Symbol('aaa') === Symbol('aaa')); //false - 무조건 새심볼임 const s1 = Symbol.for('aaa'); const s2 = Symbol.for('aaa'); console.log(s1 === s2); //true - for가 가리키는 키가 같으면 같은 심볼! console.log(Symbol.keyFor(s1)); //'aaa' - for로 만들어진 심볼은 keyFor사용가능
양쪽 검증을 위해서는 심볼을 저장하면서 동시에 키에도 저장해야 합니다. 이를 구현해보죠.
var S = function(key){ if(this instanceof S) throw 'call only'; return new Symb(key); }; var keys = {}; //키별 심볼관리 var symbols = {}; //심볼별 키관리 S.for = function(key){ var symbol; //키에 해당되는 심볼이 있는지 확인 if(key in keys) return keys[key]; //새 심볼 생성 symbol = S(); //해당 키에 심볼객체 저장 keys[key] = symbol; //해당 심볼에 키저장 symbols[symbol] = key; return symbol; }; S.keyFor = function(symbol){ //심볼별 키저장소에서 꺼내줌 return symbols[symbol]; };
딱히 어려운 점은 없으므로 설명은 주석으로 대체하겠습니다.
알려진 심볼 구현
심볼에는 well-known symbol 이란 개념이 있어서 미리 엔진에서 상수로 정의해둔 심볼이 존재합니다. 시스템내부에서 덕타입으로 호출할 때 불안정한 문자열기반의 키이름을 배제하고 내장 객체의 참조로 검증하기 위한 장치입니다. 예를들어 기존 문자열 이름으로 정의된 시스템 내부의 덕타입 인터페이스는 다음과 같은 것들이 있습니다.
- toString – 문자열로 강제 형 변환될 때 호출됨
- valueOf – 숫자로 강제 형 변환될 때 호출됨
- toJSON – JSON.stringify가 작동할 때 우선적으로 호출됨
이러한 내장 덕타입 인터페이스들은 문자열이라는 취약한 기반으로 되어있었는데
- 철자가 틀려도 작동하지 않고
- 그 이름을 다른 목적으로 사용하고 있을 수도 있으며
- 무엇보다 암묵적이기 때문에 언어의 스펙문서를 보지 않으면 이러한 덕타입이 존재한다는 사실 자체를 모릅니다.
이를 개선하기 위해 내장 인터페이스를 잘 알려진 심볼로서 제공하는데… 이것도 좀 미묘한 게
- 새롭게 도입된 이터레이터용 덕타입은 문자열 next를 채용했으며
- toJSON에 해당되는 심볼은 존재하지 않고
- toPrimitive의 동작도 기존의 대체는 아니라 새로운 형변환 시스템으로 작동합니다.
개선점이 복합적으로 작용해서 좀 혼란스러운 느낌입니다. 각 알려진 심볼이 실제 어떤 문자열인터페이스로 대체되는지에 대한 표가 제시되어있습니다. 예를 위해 Symbol.toStringTag 를 구현해보죠. 이 심볼은 Object.prototype.toString이 표시할 문자열에 영향을 주는데 객체인 경우 일반적인 표시는 “[object Object]” 인데 두번째 자리에 올 문자열을 결정할 수 있게 해줍니다. 그럼 우선 S에 키를 추가해줘야겠죠.
S.toStringTag = '@@toStringTag';
하지만 진짜는 이걸 구현한 객체를 인식해 그걸 반영해줘야한다는 것입니다. 이 복잡한 폴리필은 구현하는게 의미있는지는 모르겠지만서도 일단 해보죠.
//심볼이 없는 경우 폴리필 if(!Symbol)(function(){ //원본 toString을 잡아둔다(전체구현은 두려우니까 ^^;;) var origin = Object.prototype.toString; Object.prototype.toString = function(){ //null이 아닌 객체인 경우만 처리하자 if(!this || typeof this != 'object') return origin.call(this); //객체라도 toStringTag없으면 개무시하자. if(!('@@toStringTag' in this) || typeof this['@@toStringTag'] != 'function') return origin.call(this); return '[object ' + this['@@toStringTag']() + ']'; }; })();
이왕한 거 하나 더 해보죠(먼가 미묘한 매력이..) toPrimitive의 경우 기존의 toString과 valueOf를 복합적으로 대체합니다. 이를 구현하려면 Object.prototype에 구현된 구현물을 건드릴 수 밖에 없습니다. 구체적인 작동을 위해 스펙문서를 참조해보죠. 내용을 간단히 정리해보면..
- PreferredType을 통과 못하면 힌트는 “default”
- PreferredType이 문자열이면 힌트도 “string”
- PreferredType이 숫자면 힌트도 “number”
- @@toPrimitive로 메소드를 불러내서 힌트넘기고
- 결과가 객체타입이 아니면 반환한다.
- 객체타입이면 예외다.
이 정도입니다. 헌데 중요한건 그 다음 절차인데 4번의 @@toPrimitive가 없으면 Return OrdinaryToPrimitive(input,hint) 즉 옛날 형변환 시스템에게 위임한다죠. 이걸 응용해보죠.
if(!Symbol)(function(){ var originToString = Object.prototype.toString; var originValueOf = Object.prototype.valueOf; Object.prototype.toString = function(){ var result; //toPrimitive없으면 원래대로 가. if(!('@@toPrimitive' in this) return originToString.call(this); //힌트를 'string'으로 호출 result = this['@@toPrimitive']('string'); //객체 타입으로 배신하면 타입에러다! if(typeof result == 'object') throw new TypeError(); return result; }; //이하 자세한 설명은 생략한다. Object.prototype.valueOf = function(){ var result; if(!('@@toPrimitive' in this) return originValueOf.call(this); if(result = this['@@toPrimitive']('number'), typeof result == 'object') throw new TypeError(); return result; }; })();
그 외에도 많은 알려진 심볼의 기능을 생각해보면 패치할 수 있긴 하지만 큰 의미도 없는지라 딱히 폴리필의 필요성을 느끼고 있지 않습니다.
단지 이후 이터러블과 이터레이터 인터페이스는 구현할 예정이기 때문에 이를 위해 iterator만 정의하는 걸로 하겠습니다.
S.iterator = '@@iterator';
결론
지금까지 구현한 Symb의 최종 코드는 다음과 같습니다.
var Symb = (function(){ var uuid = (function(){ var uuid = 0; return function(){ return '@@Symbol:' + (+new Date) + ':' + (uuid++) + ':' + (Math.random() + '').substr(0, 5); }; })(); var Symb = function(key){ this.uuid = uuid(); this.key = key || ''; }; Symb.prototype.toString = function(){ return this.key + ':' + this.uuid; }; var S = function(key){ if(this instanceof S) throw 'call only'; return new Symb(key); }; var keys = {}, symbols = {}; S.for = function(key){ if(!keys[key]) symbols[keys[key] = S()] = key; return keys[key]; }; S.keyFor = function(symbol){ return symbols[symbol]; }; S.iterator = '@@iterator'; return S; })();
단 위의 코드에서 한 가지 문제점이 있습니다. 객체의 키에 예약어를 쓸 수 있는 스펙은 ES5 이후입니다. 즉 ES3.1기반의 브라우저에서는 S.for 에서 에러가 발생합니다. 거기까지 패치하려면 S[‘for’] =.. 로 정의하고 사용할 때도 S‘for’ 이런 식으로 사용하면 됩니다.
이제 유사 심볼객체를 사용할 수 환경을 구축했습니다. 다음 포스팅에서는 이를 이용해 이터러블과 이터레이터를 구축해 보겠습니다.
샘플코드는 여기에 있습니다.
recent comment