안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] 심볼 (Symbol) 타입 이해하기

피그브라더 2021. 5. 18. 21:07

JavaScript는 총 6개의 원시 타입(number, string, boolean, null, undefined, symbol)과 1개의 객체 타입(object)을 가지고 있다. 이번 포스팅에서 다룰 심볼(symbol) 타입도 6개의 원시 타입 중 하나로, ES6 버전의 JavaScript에서 새롭게 추가되었다. 결론부터 얘기하자면, 일반적으로 심볼 타입은 객체의 프로퍼티 키를 고유하게 설정함으로써 프로퍼티 키의 충돌을 방지하기 위해 사용된다. 다른 타입에 비해 흔하게 사용되는 타입은 아니지만, JavaScript에는 심볼 타입에 대한 이해가 전제되어야 이해하고 사용할 수 있는 몇몇 문법(EX. iterable 객체)들도 있기 때문에 공부해두는 것이 좋다.

 

1. 심볼의 생성 및 활용

심볼은 Symbol 함수를 호출함으로써 생성할 수 있다. 이때 생성되는 심볼은 변경이 불가능한 원시 값이다. Symbol 함수를 호출할 때 인자로 전달하는 문자열 값은 생성될 심볼에 대한 일종의 설명문(Description)으로, 오직 디버깅의 용도로만 사용된다. 즉, 뒤에서 설명할 심볼의 키와는 완전히 다른 것으로, console.log() 등을 이용한 디버깅 시에 각 심볼을 구분하기 위한 용도로 사용이 된다.

// Create symbols
const sym1 = Symbol();
const sym2 = Symbol('foo');
const sym3 = Symbol('bar');

// Print symbols (use the description that was specified when calling Symbol function)
console.log(sym1);  // Symbol()
console.log(sym2);  // Symbol(foo)
console.log(sym3);  // Symbol(bar)

// Check type of symbol
console.log(typeof sym1);  // symbol
console.log(typeof sym2);  // symbol
console.log(typeof sym3);  // symbol

 

한편, Symbol 함수를 호출하면 매번 새로운(고유한) 심볼이 생성된다. 일치 연산자(===)를 통해 이를 확인해 보자.

const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol('foo');
const sym4 = Symbol('foo');

console.log(sym1 === sym1);  // true

console.log(sym1 === sym2);  // false
console.log(sym3 === sym4);  // false

 

그런데 심볼 타입에는 특이한 점이 하나 있다. 그것은 바로 number, string, boolean 타입과 달리 new 연산자를 이용한 래퍼 객체의 생성이 불가능하다는 점이다. new 연산자를 이용하여 래퍼 객체를 생성하려고 하면 TypeError가 발생한다. new 연산자를 이용할 수 없다는 것은 곧 Symbol 함수를 생성자로 사용할 수 없음을 의미한다.

const sym = new Symbol();  // Uncaught TypeError: Symbol is not a constructor

 

number, string, boolean 타입의 경우 new 연산자를 이용한 래퍼 객체의 생성이 가능하다. 이렇게 생성되는 래퍼 객체는 해당 타입의 원시 값을 저장하고 있고, 유용한 몇몇 메소드들을 가지고 있다. 만약 new 연산자를 이용하지 않고 단순히 Number, String, Boolean 함수를 호출하기만 하면 해당 타입의 원시 값이 생성되기만 하고 래퍼 객체는 생성되지 않는다. 참고로, Array 함수의 경우에는 단순히 함수를 호출하기만 하든 new 연산자를 이용하여 생성자로서 호출하든 결과는 같다.

 

그러면 이렇게 생성된 심볼은 어디에 쓰일까? 결론부터 얘기하자면, 일반적으로 심볼은 객체의 프로퍼티 키로 사용된다. 프로퍼티 키란 곧 해당 프로퍼티의 값에 접근하고자 할 때 사용하는 이름이다. JavaScript에서 객체의 프로퍼티 키는 대개 문자열 값이다. 숫자로 쓰는 것도 사실은 문자열이다(내부적으로 문자열로 변환됨).

const obj = {};

obj.propertyKey1 = 'propertyValue1';
obj['propertyKey2'] = 'propertyValue2';

obj[3] = 'propertyValue3';  // obj['3'] = 'propertyValue3'으로 변환

console.log(obj);  // {propertyKey1: 'propertyValue1', propertyKey2: 'propertyValue2', 3: 'propertyValue3'} 

 

그런데 문자열 값 대신 심볼도 프로퍼티 키로 사용될 수 있다는 것이다. 이 경우, 기본적으로 심볼은 고유하기 때문에 심볼을 키로 갖는 프로퍼티는 다른 어떤 프로퍼티와도 충돌하지 않을 것이다.

const obj = {};

const sym1 = Symbol();
const sym2 = Symbol('foo');
const sym3 = Symbol('foo');

obj[sym1] = 'propertyValue1';
obj[sym2] = 'propertyValue2';
obj[sym3] = 'propertyValue3';  // no conflict with sym2

console.log(obj);  // {Symbol(): 'propertyValue1', Symbol(foo): 'propertyValue2', Symbol(foo): 'propertyValue3'}

console.log(obj[sym1]);  // propertyValue1
console.log(obj[sym2]);  // propertyValue2
console.log(obj[sym3]);  // propertyValue3

 

2. 내장 심볼 (Built-in Symbol)

Symbol 함수를 이용하여 직접 심볼을 생성하고 사용할 수도 있지만, 특별한 용도로 사용되기 위해 JavaScript 엔진 내에 미리 생성되어 상수로 존재하고 있는 내장 심볼(Built-in Symbol)들도 존재한다. 이들은 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];

 

iterable 객체와 관련된 자세한 설명은 이 포스팅을 참고하도록 하자. Symbol.iterator 외에도 특별한 용도로 사용되는 내장 심볼들이 여러 가지 있겠지만, 그리 중요한 내용은 아니라고 판단하여 여기서는 설명을 생략하도록 한다.

 

3. Symbol.for(), Symbol.keyFor() 메소드

Symbol 함수를 호출함으로써 생성하는 심볼들은 키를 가지고 있지 않으며, 전역 심볼 레지스트리에 저장되지도 않는다. 전역 심볼 레지스트리(Global Symbol Registry)란 심볼들이 저장되는 전역 공간을 의미하는 것이며, 여러 모듈들이 하나의 심볼을 공유하기 위한 용도로 존재한다. 여러 모듈들이 하나의 심볼을 공유하려면 그 심볼이 를 가지고 있어야 한다. 그래야 키를 통해 이미 존재하는 심볼을 찾아 재활용할 수 있기 때문이다. 이와 관련된 메소드들에 대해 한 번 알아보자.

 

3-1. Symbol.for() 메소드

인자로 전달받은 문자열 값을 키로 갖는 심볼을 전역 심볼 레지스트리에서 찾아 반환하고, 탐색에 실패한다면 그 문자열 값을 키로 갖는 심볼을 새로 생성하여 전역 심볼 레지스트리에 저장한 뒤 이를 반환한다. 단순히 Symbol 함수를 호출하여 심볼을 생성하는 것에 비해, 생성되는 심볼이 키를 갖고 있으며 전역 심볼 레지스트리에 저장이 된다는 차이점이 있다.

const sym1 = Symbol.for('foo');  // Cretate symbol
const sym2 = Symbol.for('foo');  // Reuse symbol

console.log(sym1 === sym2);  // true

 

3-2. Symbol.keyFor() 메소드

인자로 전달받은 심볼을 전역 심볼 레지스트리에서 찾고, 그 심볼의 키를 반환하고, 탐색에 실패한다면 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

 

4. for ... in 문법과 JSON.stringify() 메소드에서의 심볼

앞서 말하기를 심볼은 객체의 프로퍼티 키로 사용된다고 하였다. 그런데 주의해야 할 점이 하나 있다. 기본적으로 JavaScript가 제공하는 for ... in 문법에서 키가 심볼인 프로퍼티들은 열거되지 않는다. 또한 Object.getOwnPropertyNames() 메소드 또한 키가 심볼인 프로퍼티들은 반환하지 않는다. 이 사실은 다음 예시 코드를 통해 확인할 수 있다.

const obj = {};

obj[Symbol('a')] = 'a';
obj[Symbol.for('b')] = 'b';
obj['c'] = 'c';
obj.d = 'd';

for (const propertyKey in obj) {
   console.log(propertyKey);  // logs 'c' and 'd'
}

만약 키가 심볼인 프로퍼티들의 목록을 확인하고 싶다면 Object.getOwnPropertySymbols() 메소드를 사용하면 된다. 이 메소드는 프로퍼티의 키로 사용되는 심볼들로 이뤄진 배열을 반환한다. 일반적인 객체는 심볼이 키인 프로퍼티가 없기에 빈 배열을 반환한다.

 

또한, 객체를 JSON으로 만들 때(JSON.stringify() 메소드 호출)도 키가 심볼인 프로퍼티들은 무시된다. 이에 주의하도록 하자.

const sym = Symbol('foo');
const obj = {
    [sym]: 'propertyValue1',
    propertyKey2: 'propertyValue2'
};

JSON.stringify(obj);  // {"propertyKey2":"propertyValue2"}

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol

https://poiemaweb.com/es6-symbol