본문 바로가기

개발 이야기/front-end

Symbol을 사용하는 이유는 뭘까 | symbol usage

javascript

 

 

ECMAScript로 표준화된 이후로 자바스크립트에는 string, number, boolean, null, undefined, object 6개의 데이터 타입이 있었다. 그리고 es6에서 새로운 데이터 타입 Symbol이 추가 되었다. symbol은 무엇이고 언제 사용하는 걸까?

 


 

What is Symbol?

 

심볼, Symbol

심볼은 변경 불가능한 원시 타입의 값이며, 다른 값과 중복되지 않는 고유한 값이다. 심볼 값은 충돌 위험이 없는 오브젝트의 유일한 프로퍼티 키를 만들기 위해서 사용할 수 있다. 하위호환성을 유지하면서 표준을 확장한다든지, 고유한 상수값을 만드는 데 사용할 수 있다.

const symbolA = Symbol('symbol') 
const symbolB = Symbol('symbol')

console.log(symbolA === symbolB) // false

*Ruby의 Symbol과는 다르다.

*Ruby의 Symbol: 변경 불가능하며, 다른 데이터 타입과는 달리 동일한 객체를 여러번 생성하지 않고 한 객체를 재사용할 수 있게 함으로써 메모리 낭비를 줄임.

 

 

Symbol 함수

Symbol 값은 Symbol 함수를 호출하여 생성한다. 심볼 값은 자바스크립트 런타임 환경에서 Symbol 함수에 의해 동적으로 생성되며 다른 값과 중복되지 않는 고유한 값이다. 생성된 심볼 값은 외부로 노출되지 않아 확인할 수 없다.

 

Symbol 함수에 들어가는 문자열 인자는 심볼 값에 대한 description으로서 선택적으로 넣을 수 있다. 이 문자열은 디버깅 용도로만 사용되며 심볼 값 생성에 영향을 주지는 않는다.

 

심볼 값도 객체처럼 메서드를 사용하면 암묵적으로 *래퍼 객체를 생성한다. description 프로퍼티와 toString은 Symbol.prototype의 프로퍼티다. 심볼 값은 문자열이나 숫자 타입으로 변환되지 않는다. 단 불리언 타입으로는 타입 변환이 된다.

 

const a = Symbol('a')
a.description // "a"
a.toString() // "Symbol(a)"
!a // false
a + 'hi' // Uncaught TypeError

*래퍼 객체: 일시적으로 임시 객체를 만들어서 메서드나 프로퍼티를 사용할 수 있게 한 후, 역할이 끝나면 다시 원시값으로 돌아온다. 사용이 끝난 래퍼 객체는 가비지 컬렉션의 대상이 된다.

 

 

 

전역 심볼 레지스트리, global symbol registry

자바스크립트 엔진이 관리하는 전역 심볼 레지스트리는 사용 가능한 모든 심볼이 저장되어 있다. 레지스트리에 접근할 수 있는 함수로는 Symbol.for, Symbol.keyFor 메서드가 있다. 이 메서드들은 전역 심볼 레지스트리 테이블과 런타임 환경 사이에서 심볼 값을 전해주는 역할을 한다. 전역 심볼 레지스트리는 대부분 자바스크립트 컴파일 인프라에 내장되어 있고, 레지스트리 내용은 자바스크립트 런타임 환경에서는 Symbol.for, Symbol.keyFor 메서드를 사용하지 않고서는 접근이 불가능하다.

 

 

 

Symbol.for, Symbol.keyFor

Symbol.for 메서드는 인수로 전달받은 문자열을 키로 사용해 전역 심볼 레지스트리에 해당 키와 일치하는 심볼 값을 검색한다. 레지스트리에 이미 심볼이 있으면 해당 심볼을 반환하고, 없으면 새로 생성하여 반환한다.

 

Symbol.keyFor 메서드는 심볼 값을 인수로 받아서 전역 심볼 레지스트리에 저장된 심볼 값 키를 가져올 수 있다.

 

const symA = Symbol.for('a')
const symB = Symbol.for('a')
symA === symB // true
Symbol.keyFor(symA) // a

 

Symbol 함수는 호출될 때마다 심볼 값을 생성하지만, 전역 심볼 레지스트리에서 관리할 때 필요한 키를 지정할 수 없으므로(인수로 전달하는 문자열 키는 description일 뿐이다) 전역 심볼 레지스트리에 등록되어 관리되지 않는다. 그러나 Symbol.for 메서드를 사용하면 문자열 키를 통해 전역에서 중복되지 않는 유일무이한 심볼 값을 하나만 생성하여, 전역 심볼 레지스트리를 통해 값을 공유할 수 있다.

 

 


 

Symbol Usage

 

1. 심볼과 상수: 자바스크립트에서 enum 사용하기

enum처럼 값에는 의미가 없고, 상수 이름 자체에 의미가 있는 경우가 있다. 상수 값은 변경될 수 있으며 다른 변수 값과 중복될 수 있다는 문제가 있다. 변경/중복될 가능성이 있는 무의미한 상수 대신 중복될 가능성이 없는 유일무이한 심볼 값을 사용할 수 있다.

자바스크립트에서 enum을 사용하려면 객체 변경을 방지하기 위해 객체를 동결하는 Object.freeze 메서드와 심볼 값을 사용한다.

const Direction = Object.freeze({
	UP: Symbol('up'),
	DOWN: Symbol('down'),
	LEFT: Symbol('left'),
	RIGHT: Symbol('right'),
})

 

Facebook/React 프로젝트 hydration.js 파일

export const meta = {
  inspectable: Symbol('inspectable'),
  inspected: Symbol('inspected'),
  name: Symbol('name'),
  preview_long: Symbol('preview_long'),
  preview_short: Symbol('preview_short'),
  readonly: Symbol('readonly'),
  size: Symbol('size'),
  type: Symbol('type'),
  unserializable: Symbol('unserializable'),
};

*hydrate: 서버사이드렌더링으로 정적인 HTML이 만들어진 상태에서 동적으로 기능할 수 있도록 이벤트 핸들러만 붙여주는 작업. 이미 그림이 그려진 상태이므로 렌더링을 따로 하지 않아 더 빨리 로드될 수 있다.

 

 

 

2. 심볼과 프로퍼티 키: 프로퍼티 은닉하기

심볼 값으로 고유한 프로퍼티 키를 만들 수 있다.

const obj = {
  [Symbol.for('mySymbol')]: 1
};

obj[Symbol.for('mySymbol')]; // 1

 

 

심볼 값을 프로퍼티 키로 사용하면 for ... in이나 Object.keys, Object.getOwnPropertyNames 메서드로 찾을 수 없다. 따라서 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있다. 단, es6에 도입된 Object.getOwnPropertySymbols 메서드를 사용하면 심볼 값을 프로퍼티 키로 사용하여 생성한 프로퍼티를 찾을 수 있다.

 

const obj = {
  [Symbol.for('mySymbol')]: 1
};

Object.getOwnPropertySymbols(obj); // [Symbol(mySymbol)]

 

 

Facebook/React 프로젝트 utils.js 파일. enum형의 객체에서 Symbol을 포함한 모든 키를 가져올 때 사용하고 있다.

export function getAllEnumerableKeys(
  obj: Object,
): Set<string | number | Symbol> {
  const keys = new Set();
  let current = obj;
  while (current != null) {
    const currentKeys = [
      ...Object.keys(current),
      ...Object.getOwnPropertySymbols(current),
    ];
    
    // ...생략
  }
  return keys;
}

 

 

3. 심볼과 표준 빌트인 객체 확장

빌트인 객체에 사용자 정의 메서드를 직접 추가할 때, 다음 ECMAScript에 똑같은 이름의 메서드가 도입된다면 문제가 된다. 따라서 중복될 가능성이 없는 심볼 값으로 빌트인 객체를 확장하면 나중에 추가될지 모르는 어떤 메서드와도 충돌할 위험이 없어 안전하게 빌트인 객체를 확장할 수 있다.

String.prototype[Symbol.for('caseInsensitiveSearch')] = function (target) {
  return this.toLowerCase().indexOf(target);
}

'aaaBar'[Symbol.for('caseInsensitiveSearch')]('bar'); // 3

 

 

 

4. Well-known Symbol: 표준 확장 또는 이터러블 타입 체킹

Well-Known Symbol은 자바스크립트 기본으로 제공하는 표준 빌트인 심볼 값이다.

 

이 값은 자바스크립트 엔진 내부 알고리즘에 사용된다. 순회 가능한 빌트인 이터러블은 Well-Known Symbol인
Symbol.iterator를 키로 갖는 메서드를 가지며, Symbol.iterator 메서드를 호출하면 이터레이터를 반환
하도록 ECMAScript 사양에 규정되어 있다.

빌트인 이터러블은 이터레이션 프로토콜을 준수한다. 만약 빌트인 이터러블이 아닌 일반 객체를 이터러블처럼 동작하도록 구현하고 싶으면 이터레이션 프로토콜을 따르면 된다.

— modern javascript deep dive, p.612

 

Symbol 클래스는 Well-Known Symbol이라고 불리는 상수들을 갖는다. 이 심볼 값을 통해 자바스크립트가 어떻게 심볼 값을 속성 키로 사용해 객체를 다루는지 알 수 있다. Symbol.iterator를 통해 순회 가능한 object를 만들거나, Symbol.search를 통해 객체에 문자열의 인덱스를 찾는 메서드를 추가할 수 있다. 그 외에도 아래와 같은 심볼 값들이 있으며 이 값들을 통해 표준을 확장할 수 있다.

 

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive

...등

 

 

표준을 확장하는 것 말고도 이터러블 타입 체킹도 할 수 있다. typeof data === Array, typeof data === Map 조건을 일일이 열거할 필요없이 typeof data[Symbol.iterator]를 사용하면 된다. 위에서 언급된 것처럼 빌트인 이터러블은 ECMAScript 사양에 따라 Symbol.iterator를 키로 갖는 메서드를 갖고 있기 때문이다.

 

facebook/React 프로젝트 utils.js 파일

export function getInObject(object: Object, path: Array<string | number>): any {
  return path.reduce((reduced: Object, attr: any): any => {
    if (reduced) {
      if (hasOwnProperty.call(reduced, attr)) {
        return reduced[attr];
      }
      if (typeof reduced[Symbol.iterator] === 'function') {
        return Array.from(reduced)[attr];
      }
    }

    return null;
  }, object);
}