개요
es6에 도입된 Symbol과 계산된 객체키라는 기능은 활용하기에 따라서 여러가지 용도가 있습니다. 이번 포스팅에서는 다양한 Symbol의 용도에 대해 가볍게 살펴보고 private필드에 응용하는 안을 생각해봅니다.
프로토콜로서의 심볼
심볼은 잘 알려진 심볼(well-known symbol)이라 해서 미리 상수로 제공되는 다양한 심볼이 시스템에 처음부터 존재합니다. 다음과 같은 것들이 있죠.
- Symbol.iterator
- Symbol.match
- Symbol.search
- Symbol.replace
- Symbol.split
그 외에도 hasInstance, species, unscopables, isContactSpreadable, toPrimitive, toStringTag 등이 있습니다. 이름만 봐도 감잡으셨을테지만 태반 엔진이 사용하고 키입니다.
하지만 이 키가 상수로 정의되어 있다는 것 자체가 일종의 타입클래스라는 효과를 가져옵니다. 예를 들어 이터러블의 경우 해당 객체가 Symbol.iterator를 갖으면 해당 프로토콜을 따르는 것으로 간주합니다. 약한 덕타입의 프로토콜 규약을 통해 범용객체인 Object가 특정 목적에 따라 만들어졌다는 것을 보장하는 식으로 사용되는 것이죠.
따라서 기존 클래스 언어처럼 Iterator를 상속한 클래스를 만드는게 아니라 그저 그 객체의 키에 Symbol.iterator가 존재하면 되는 것이죠.
실제 for..of 에서 작동하는 이터러블+이터레이터 객체는 단순히 next와 Symbol.iterator가 있기만 하면 됩니다(그러면서 next는 왜 단순키로 한건지 이해가 안되긴 합니다만 ^^)
let test = { cnt:0, next(){return {value:test.cnt++, done:test.cnt > 10};}, [Symbol.iterator](){return test;} }; for(let v of test) console.log(v);
위 코드에서 test는
- next를 구현하여 이터레이터 프로토콜을 준수하고 있으며,
- Symbol.iterator를 통해 자신을 반환함으로서 이터레이터 객체를 반환해야하는 이터러블 프로토콜을 만족시키고 있습니다.
이러한 프로토콜이라는 개념은 ES6전반에 쓰이게 되는데 super를 위한 Symbol.species를 비롯하여 눈에 보이지 않은 많은 프로토콜을 내장하고 있습니다.
또한 Symbol은 객체키이므로 이러한 프로토콜은 유저가 정의하여 사용할 수 있습니다. 본격 프로토콜지향 프로그래밍으로 넘어갈 수 있는 수단을 제공하고 있는 셈이죠.
private속성을 위한 심볼
콘솔로는 보일지 몰라도 중첩영역이나 스코프를 통해 가려진 Symbol객체는 외부에서 사용할 방법이 없습니다. 예를들어 볼까요.
let test; { let name = Symbol(); test = { [name]:'hika' }; } console.log(test);
찍어보면 Object {Symbol(): “hika”} 정도로 나옵니다만, 실제 프로그래밍상에서 저 hika라는 값을 얻거나 수정할 방법은 없습니다. name이라는 심볼객체에 접근할 수 없기 때문이죠. 이를 이용하면 접근을 제한할 수 있는 강력한 private기능을 구현할 수 있습니다. 간단히 전용 메소드로만 컨트롤 할 수 있는 속성을 하나 정의해보죠.
let test; { let state = Symbol(); test = { [state]: false, on(){this[state] = true;}, off(){this[state] = false;}, get state(){return this[state];} }; } console.log(test.state); //false test.on(); console.log(test.state); //true test.off(); console.log(test.state); //false
위의 예에서 핵심은 state라는 Symbol에 접근할 수 있는 권한이 test내부에만 있기 때문에 this[state]를 외부에서는 바꿀 수 없다는 점입니다.
이는 클래스로 확장하여 사용하면 더욱 위력이 강력해집니다. 간단한 이벤트 디스패처를 하나 작성해보죠.
이벤트별로 리스너를 담을 오브젝트가 필요할 것입니다. 하지만 이벤트용 메소드만으로 컨트롤하게 해야하므로 리스너를 담고 있는 객체를 외부에서 조작할 수 없도록 조치해야죠. 기존의 복잡한 스코프구조를 통해 별도의 객체를 인식하지 않게 하고도 간단히 해당 리스너컨테이너를 담는 키를 은닉된 Symbol로 만드는 것만으로 해결됩니다.
(Symbol은 Object.defineProperty의 두번째 인자로도 사용될 수 있습니다)
let Dispatcher; { let LISTENER = Symbol(); //리스너용 심볼 Dispatcher = class{ constructor(){ //생성시점에 리스너컨테이너생성 Object.defineProperty(this, LISTENER, {value:{}}); } addListener(eventType, listener){ let target = this[LISTENER][eventType]; //해당 이벤트명으로 set이 없으면 생성해줌 if(target){ this[LISTENER][eventType] = target = new Set(); } //리스너추가 target.add(listener); } removeListener(eventType, listener){ let target = this[LISTENER][eventType]; if(target && typeof listener == 'function'){ target.delete(listener); //리스너제거 } } dispatch(eventType){ let target = this[LISTENER][eventType]; if(target){ //해당 이벤트의 리스너를 쭉 호출해줌 for(let listener of target) listener(); } } } let test = new Dispatcher(); test.addListener('test', ()=>console.log('listen')); test.dispatch('test'); //'listen'
위의 코드에서 this[LISTENER]에 접근하여 추가하거나 삭제하거나 혹은 그 안의 데이터에 접근할 수 있는 건 오직 클래스의 메서드로 한정됩니다. 데이터는 분명 그 인스턴스에 존재하지만 그 데이터를 조작할 권한이 없는 것이죠. 단지 이 패턴은 클래스에 쓸 때 약간 귀찮은 측면이 있습니다. 많은 클래스는 let으로 정의되지 않고 const로 정의됩니다. 그 경우 let처럼 선언한 뒤 중괄호내의 정의가 불가능하니 즉시 실행함수 패턴으로 결합해야 합니다.
const Dispatcher = (()=>{ let LISTENER = Symbol(); return class{ //상동 }; })();
이런 느낌이 되죠.
결론
Symbol 첫번째 포스팅에선 간단한 의미와 활용을 짚어봤습니다. 이후의 포스팅에서 시스템이 제공하는 상수 심볼별 의미를 더욱 자세히 살펴보고 다양한 Symbol의 활용을 생각해보겠습니다.
recent comment