자바스크립트는 총 6개의 원시 타입(number, string, boolean, null, undefined, symbol)과 1개의 객체 타입(object)을 가지고 있다. 이번 포스팅에서 다룰 심볼(Symbol)
타입도 6개의 원시 타입 중 하나로, ES6 버전의 자바스크립트에서 새롭게 추가되었다. 결론부터 얘기하자면, 일반적으로 심볼 타입은 객체의 프로퍼티 키를 고유하게 설정함으로써 프로퍼티 키의 충돌을 방지하기 위해 사용된다.
심볼은 Symbol 함수를 호출함으로써 생성할 수 있다. 이때 생성되는 심볼은 변경이 불가능한 원시 값이다. Symbol 함수를 호출할 때 인자로 전달하는 문자열 값은 생성될 심볼에 대한 일종의 설명문으로, 오직 디버깅의 용도로만 사용된다. 즉, 뒤에서 설명할 심볼의 키와는 완전히 다른 것으로, console.log()
등을 이용한 디버깅 시에 각 심볼을 구분하기 위한 용도로 사용된다.
const sym1 = Symbol();
const sym2 = Symbol('symbol test');
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(symbol test)
console.log(typeof sym1); // symbol
console.log(typeof sym2); // symbol
한편, Symbol 함수를 호출하면 매번 새로운(고유한) 심볼이 생성된다.
const sym1 = Symbol('test');
const sym2 = Symbol('test');
console.log(sym1 === sym2); // false
그런데 심볼 타입에는 특이한 점이 하나 있다. 그것은 바로 number, string, boolean 타입과 달리 new 연산자를 이용한 래퍼 객체의 생성이 불가능하다는 점이다. new 연산자를 이용하여 래퍼 객체를 생성하려고 하면 TypeError가 발생한다. new 연산자를 이용할 수 없다는 것은 곧 Symbol 함수를 생성자로 사용할 수 없음을 의미한다.
const sym = new Symbol(); // Uncaught TypeError
number, string, boolean 타입의 경우 new 연산자를 이용한 래퍼 객체의 생성이 가능하다. 이렇게 생성되는 래퍼 객체는 해당 타입의 원시 값을 저장하고 있고, 유용한 몇몇 메소드들을 가지고 있다. 만약 new 연산자를 이용하지 않고 단순히 Number, String, Boolean 함수를 호출하기만 하면 해당 타입의 원시 값이 생성되기만 하고 래퍼 객체는 생성되지 않는다. 참고로, Array 함수의 경우에는 단순히 함수를 호출하기만 하든 new 연산자를 이용하여 생성자로서 호출하든 결과는 같다.
그러면 이렇게 생성된 심볼은 어디에 쓰일까? 결론부터 얘기하자면, 일반적으로 심볼은 객체의 프로퍼티 키로 사용된다. 프로퍼티의 값에 접근하고자 할 때 사용하는 이름이다. 자바스크립트에서 객체의 프로퍼티 키는 대개 문자열 값이다. 숫자로 쓰는 것도 사실은 문자열이다(내부적으로 문자열로 변환됨).
그런데 문자열 값 대신 심볼도 프로퍼티 키로 사용될 수 있다는 것이다. 이 경우, 기본적으로 심볼은 고유하기 때문에 심볼을 키로 갖는 프로퍼티는 다른 어떤 프로퍼티와도 충돌하지 않을 것이다.
const obj = {};
const sym = Symbol('foo');
obj[sym] = '123';
console.log(obj); // {Symbol(foo): '123'}
console.log(obj[sym]); // 123
Symbol 함수를 이용하여 직접 심볼을 생성하고 사용할 수도 있지만, 특별한 용도로 사용되기 위해 자바스크립트 엔진 내에 미리 생성되어 상수로 존재하고 있는 내장 심볼들도 존재한다. 이들은 Symbol 함수의 프로퍼티로서 존재한다.
내장 심볼의 가장 대표적인 예시가 바로 Symbol.iterator이다. JavaScript 엔진은 이 심볼을 키로 갖는 메소드가 정의된 객체를 iterable 객체로 인식한다. iterable 객체로 인식되는 객체들만 for ... of 문법 등을 이용한 반복이 가능하다. Symbol.iterator를 키로 갖는 메소드를 정의해둔 내장 iterable 객체들의 예시로는 다음과 같은 것들이 있다.
Array.prototype[Symbol.iterator];
String.prototype[Symbol.iterator];
Map.prototype[Symbol.iterator];
Set.prototype[Symbol.iterator];
arguments[Symbol.iterator];
NodeList.prototype[Symbol.iterator];
HTMLCollection.prototype[Symbol.iterator];
Symbol 함수를 호출함으로써 생성하는 심볼들은 키를 가지고 있지 않으며, 전역 심볼 레지스트리에 저장되지도 않는다. 전역 심볼 레지스트리란 심볼들이 저장되는 전역 공간을 의미하는 것이며, 여러 모듈들이 하나의 심볼을 공유하기 위한 용도로 존재한다. 여러 모듈들이 하나의 심볼을 공유하려면 그 심볼들이 키를 가지고 있어야 한다. 그래야 키를 통해 이미 존재하는 심볼들을 찾아 재활용할 수 있기 때문이다. 이와 관련된 메소드들에 대해 한 번 알아보자.
인자로 전달받은 문자열 값을 키로 갖는 심돌을 전역 심볼 레지스트리에서 찾아 반환하고, 탐색에실패한다면 그 문자열 값을 키로 갖는 심볼을 새로 생성하여 전역 심볼 레지스트리에 저장한 뒤 이를 반환한다. 단순히 Symbol 함수를 호출하여 심볼을 생성하는 것에 비해, 생성되는 심볼이 키를 갖고 있으며 전역 심볼 레지스트리에 저장이 된다는 차이점이 있다.
const sym1 = Symbol.for('foo');
const sym2 = Symbol.for('foo');
console.log(sym1 === sym2); // true
인자로 전달받은 심볼을 전역 심볼 레지스트리에서 찾고, 그 심볼의 키를 반환하고, 탐색에 실패한다면 undefined를 반환합니다. 이 함수를 이용하여 3-1에서 설명한 내용을 검증해보자. 즉, Symbol 함수를 호출하여 심볼을 생성하는 것과 Symbol.for() 메소드를 호출하여 심볼을 생성하는 것의 차이점을 확인해보자.
const unsharedSym = Symbol('foo');
const symKey1 = Symbol.keyFor(unsharedSym);
console.log(symKey1); // undefined
const sharedSym = Symbol.for('foo');
const symKey2 = Symbol.keyFor(sharedSym);
console.log(symKey2); // foo
앞서 말하기를 심볼은 객체의 프로퍼티 키로 사용된다고 하였다. 그런데 주의해야 할 점이 하나 있다. 기본적으로 자바스크립트가 제공하는 for ... in 문법에서 키가 심볼인 프로퍼티들은 열거되지 않는다.또한 Object.getOwnPropertyNames() 메소드 또한 키가 심볼인 프로퍼티들은 반환하지 않는다.
const obj = {};
obj[Symbol('a')] = 'a';
obj['b'] = 'b';
for (const propertyKey in obj) {
console.log(property); // 'b'
}
만약 키가 심볼인 프로퍼티들의 목록을 확인하고 싶다면 Object.getOwnPropertySymbols() 메소드를 사용하면 된다. 이 메소드는 프로퍼티의 키로 사용되는 심볼들로 이뤄진 배열을 반환한다. 일반적인 객체는 심볼이 키인 프로퍼티가 없기에 빈 배열을 반환한다.
또한, 객체를 JSON으로 만들 때도 키가 심볼인 프로퍼티들은 무시된다.
const sym = Symbol('foo');
const obj = {
[sym]: 'propertyValue1',
propertyKey2: 'propertyValue2'
};
JSON.stringify(obj);
enum처럼 값에는 의미가 없고, 상수 이름 자체에 의미가 있는 경우가 있다. 상수 값은 변경될 수 있으며 다른 변수 값과 중복될 수 있다는 문제가 있다. 변경/중복될 가능성이 있는 무의미한 상수 대신 중복될 가능성이 없는 유일무이한 심볼 값을 사용할 수 있다. 자바스크립트에서 enum을 사용하려면 객체 변경을 방지하기 위해 객체를 동결하는 Object.freeze
메서드와 심볼 값을 사용한다.
const Direction = Object.freeze({
UP: Symbol('up'),
DOWN: Symbol('down'),
LEFT: Symbol('left'),
RIGHT: Symbol('right'),
})
const obj = {
[Symbol.for('mySymbol')]: 1
};
obj[Symbol.for('mySymbol')]; // 1
심볼 값을 프로퍼티 키로 사용하면 for ... in 이나 Object.getOwnPropertyNames 메서도로 찾을 수 없다. 따라서 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있다. 단, ES6에 도입된 Object.getOwnPropertySymbols 메서드를 사용하면 심볼 값을 프로퍼티 키로 사용하여 생성한 프로퍼티를 찾을 수 있다.
const obj = {
[Symbol.for('mySymbol')]: 1
}
Object.getOwnPropertySymbols(obj); // [Symbol(mySymbol)]
빌트인 객체에 사용자 정의 메서드를 직접 추가할 때, 다음 ECMAScript에 똑같은 이름의 메서드가 도입된다면 문제가 된다. 따라서 중복될 가능성이 없는 심볼 값으로 빌트인 객체를 확장하면 나중에 추가될지 모르는 어떤 메서드와도 충돌할 위험이 없어 안전하게 빌트인 객체를 확장할 수 있다.
String.prototype[Symbol.for('caseInsensitiveSearch')] = function (target) {
return this.toLowerCase().indexOf(target);
}
`aaaBar`[Symbol.for('caseInsensitiveSearch')]('bar'); // 3
Well-Known Symbol은 자바스크립트 기본으로 제공하는 표준 빌트인 심볼 값이다. 이 값은 자바스크립트 엔진 내부 알고리즘에 사용된다. 순회 가능한 빌트인 이터러블은 Well-Known Symbol인 Symbol.iterator를 키로 갖는 메서드를 가지며, Symbol.iterator 메서드를 호출하면 이터레이터를 반환하도록 ECMAScript 사양에 규정되어 있다.
빌트인 이터러블은 이터레이션 프로토콜을 준수한다. 만약 빌트인 이터러블이 아닌 일반 객체를 이터러블처럼 동작하도록 구현하고 싶으면 이터레이션 프로토콜을 따르면 된다. 즉, ECMAScript 사양에 규정되어 있는 대로 Well-known Symbol인 Symbol.iterator
를 키로 갖는 메서드를 객체에 추가하고 이터레이터를 반환하도록 구현하면 그 객체는 이터러블이 된다.
const iterable = {
[Symbol.iterator]() {
let cur = 1;
const max = 5;
return {
next() {
return {value: cur++, done: cur > max + 1};
}
};
}
}
for (const num of iterable) {
console.log(num); // 1 2 3 4 5
}