Javascript 7번째 타입 Symbol을 알아보자 😎

안녕하세요! 두두코딩 입니다 ✋
오늘은 Javascript Symbol 개념에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

Symbol 이란?

1997년 자바스크립트가 ECMAScript로 표준화된 이래로 자바스크립트에는 6개의 타입 즉, 문자열, 숫자, 불리언, undefined, null, 객체 타입이 있었다.

Symbol은 ES6에서 도입된 7번째 데이터 타입으로 “변경 불가능한 원시 타입의 값” 즉, 어떤 값과도 중복되지 않는 유일무이한 값을 나타낸다.

Symbol은 이름 충돌의 위험이 있는 유일한 프로퍼티 키를 만들기 위해 사용 한다.

Symbol 값의 생성

Symbol값은 함수 호출을 통해 생성한다. 일반적인 타입들은 “리터럴” 표기법을 통해 값을 생성할 수 있지만 Symbol 값은 Symbol()를 통해서만 생성 가능하다.

Symbol 값은 외부로 노출되지 않아 확인이 불가하며, 위에서 언급한것과 같이 중복되지 않는 유일무이한 값이다.

const mySymbol = Symbol();
console.log(typeof mySymbol);

// 값 자체가 노출되지 않는다.
console.log(mySymbol);

Symbol은 함수 생성자가 아니다.

함수 호출을 통해 생성해 간혹 “함수생성자”와 같은 방식으로 생성하는 것 같아 보이지만, new 연산자를 사용하지 않는다는 점에서 우리는 함수 생성자를 호출 하지 않는다는 점을 알 수 있다.

예를들어 아래와 같이 new연산자로 생성할 경우 아래와 같은 에러를 만날 수 있다.

/*
VM2310:1 Uncaught TypeError: Symbol is not a constructor
    at new Symbol (<anonymous>)
    at <anonymous>:1:1
*/

new Symbol();

Symbol의 Wrapper 객체

Symbol 값도 숫자, 문자열, 불리언과 같이 객체처럼 접근할 경우 Wrapper 객체를 생성한다. 여기서 말하는 객체처럼 접근은 dot 연산자를 활용하는 것을 말한다.

Symbol의 wrapper 객체는 descriptiontoString 값을 가지고 있다. 아래의 예시를 통해 확인해보자.

const mySymbol = Symbol('mySymbol');

console.log(mySymbol.description); // mySymbol
console.log(mySymbol.toString()); // Symbol(mySymbol)

Symbol 값의 암묵적 변환

Symbol 값의 암묵적 변환은 구별해서 봐야한다.

아래의 예시와 같이 불리언 타입으로는 암묵적 변환이 된다. 보통 불리언 값으로 변환하고 싶을 경우 ! 연산자를 활용한다는 점을 상기시키자.

const mySymbol = Symbol();

// 불리언 타입으로는 암묵적 변환 가능
console.log(!!mySymbol); // true

if (mySymbol) console.log('mySymbol is not empty!');

문자열, 숫자 같은 경우 암묵적 변환이 불가능하다. 보통 문자열을 암묵적 변환하기 위해서는 + 연산자를 활용한다는 점을 아래의 예시를 통해 상기시키자.

const mySymbol = Symbol();

// VM957:3 Uncaught TypeError: Cannot convert a Symbol value to a string
console.log(mySymbol + '');

// Uncaught TypeError: Cannot convert a Symbol value to a number
console.log(+mySymbol);

Symbol.for / Symbol.KeyFor 메서드

Symbol.for 메서드는 인수로 전달받은 문자열을 키로 사용하여 키와 심벌 값의 쌍들이 저장되어 있는 전역 심벌 레지스트리에서 해당 키와 일치하는 심벌 값을 검색할 때 사용한다.

V8 내부 코드 확인

// v8 내부 자바스크립트 같은 경우 Table로 해당 key들을 관리하고 있다.
SymbolFor(RootIndex::kPublicSymbolTable, key, false);

보통 동일한 Symbol 값을 사용할때 해당 메서드를 많이 사용한다.

// id를 사용한다고 해보자.
const id = Symbol("id");
const s1 = Symbol.for('id');
const s2 = Symbol.for('id');

// true
console.log( s1 === s2 );

위의 코드와 같이, id라는 symbol 값을 전역 심벌 레지스트리에 등록해두고 사용할 경우 혹 id 변수가 어플리케이션 내에서 변경되거나 하더라도 우리는 id라는 키 값으로 Symbol값을 찾을 수 있다.

KeyFor 메서드는 이름에서 유추할 수 있듯이 Symbol의 key값을 찾을 때 사용한다. 아래의 예시를 통해 알아보자.

const s1 = Symbol.for('mySymbol');

// key 값 유추 가능
console.log(Symbol.keyFor(s1));

const s2 = Symbol('foo');

// key 등록되어있지 않아 유추 불가능.
console.log(Symbol.keyFor(s2));

Symbol과 상수

우리가 Symbol 타입을 사용할 경우가 property key 값을 유일무이한 값으로 저장할 때 말고 또 있을까?

또 있다. 바로 상수값을 정의할 때 사용하곤 한다. 예를들어 4방향 즉, 위, 아래, 왼쪽, 오른쪽을 나타내는 상수 값을 정의 했다고 해보자.

const Direction = {
	UP : 1,
	DOWN : 2,
	LEFT : 3,
	RIGHT : 4
};

const myDirection = Direction.UP;

if (myDirection == Direction.UP) {
	console.log('Since Go UP');
}

위의 예제와 같이 Direction 같은 경우 상수 값들을 갖고 있는 객체이다. 해당 객체의 프로퍼티 중 값에는 특별한 의미가 없고, 상수 이름 자체에 의미가 있는 경우들이 있다.

이때 문제는 1, 2, 3, 4라는 값이 변경될 수 있고, 또 다른 변수 값과 중복될 수 있다는 것이다. 이런 경우 변경 / 중복될 가능성을 배제하기 위해 Symbol을 사용한다.

// Symbol.for 보다는 그냥 symbol이 나음.
const Direction = {
	UP : Symbol('up'),
	DOWN : Symbol('down'),
	LEFT : Symbol('left'),
	RIGHT : Symbol('right')
};

const myDirection = Direction.UP;

if (myDirection == Direction.UP) {
	console.log('Since Go UP');
}

Tip 해당 기법은 다른 언어에서 enum 이라는 클래스를 활용한다. 하지만 js에서는 enum을 지원하지 않기 때문에 다른 객체의 중복을 방지하기 위해 Symbol을 사용한다.

Symbol과 프로퍼티 값

객체의 프로퍼티 키는 빈 문자열을 포함하는 모든 문자열 또는 심벌 값으로 만들 수 있다. 또한 동적으로 생성 가능한데, 아래의 예시를 통해 알아보자.

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

// output is 1
console.log(obj[Symbol.for('mySymbol')]);

obj[Symbol.for('mySymbol2')] = 2;
//output is 2
console.log(obj[Symbol.for('mySymbol2')]);

위의 예시와 같이 사용하는 방법은 대괄호를 사용한다는 점을 기억하자.

Symbol과 프로퍼티 은닉

Symbol 값은 위에서 언급했듯이 외부에 노출되지 않는다. 특히 프로퍼티의 키 값으로 사용할 때 은닉된다는 점을 기억할 필요가 있다.

보통 프로퍼티는 for ... in문이나 Object.keys, Object.getOwnPropertyNames라는 메서드를 활용해 key 값을 순회하곤 한다. Symbol을 키값으로 사용할 경우 해당 메서드를 통해 접근이 불가능하다. 즉, 은닉 시킬 수 있다는점을 기억하자.

 const obj = {
	 [Symbol('mySymbol')]: 1,
	 'stringKey' : "this is key"
 };

 for (const key in obj)
	 console.log(key);

 console.log(Object.keys(obj));

Tip ES6에 도입된 getOwnPropertySymbol 메서드를 활용하면 symbol 값을 찾을 수 있다. (왜 도입했을까? 이 부분은 조사가 필요할 듯.)

Symbol과 표준 빌트인 객체 확장

개인적으로 해당 부분이 Symbol의 가장 큰 장점이라고 생각한다.

일반적으로 표준 빌트인 객체에 사용자 정의 메서드를 직접 추가하여 확장하는 것을 권장하지 않는다. 예를들어 아래와 같이 test() 메서드를 추가해 사용한다고 가정해보자.

Array.prototype.test = function() {
	return 'test';
};

[1, 2].test();

위의 예시와 같이 구성할 경우 어떤 문제가 발생할까?

훗날 표준 빌트인 객체로 test()가 만들어질 경우 기존에 내가 직접만든 test 메서드와 충돌이 날 수 있다 실제 Array.prototype.find 메서드 같은 경우 이런 케이스가 많이 존재했는데, ES6 이전에는 find 메서드가 따로 없어 위와 같이 만들어 사용했다. 하지만 ES6이후 도입된 find 메서드로 이름 충돌이 발생했다.

이를 막기 위해서는 어떻게 해야할까? 바로 Symbol로 등록을 해두는 것이다. 중복되는 케이스가 없기 때문에 안전하게 표준 빌트인 객체 확장을 할 수 있다.

Array.prototype[Symbol.for('test')] = function() {
    return 'test';
};

[1, 2][Symbol.for('test')];

Well-known Symbol

자바스크립트가 기본으로 제공하는 빌트인 Symbol 값들이 있다. 해당 값들은 Symbol 함수의 프로퍼티에 할당 되어져 있다. 아래의 그림을 참고하자. Symbol이라고 적힌 부분이 Well-known Symbol로 구현된 것들이다.

Symbol_console_dir

ECMAScript에서 @@ 표기는 Well-known Symbol을 뜻한다.

ECMASCript_symbol

자바스크립트가 기본 제공하는 빌트인 Symbol 값을 ECMAScript에서 정해뒀으며 자바스크립트 엔진의 내부 알고리즘을 통해 구현된다.

Well-known Symbol은 간단하게 말하면 내장 함수이고, 기본적으로 사용하는 함수라고 생각하면된다. 따라서, 다양한 빌트인 객체에서 사용하고 있다. 일례로 for ... of를 동작하게 하는 함수를 Symbol.iterator로 구현해두고, 해당 Symbol 값을 다양한 빌트인 객체 예를들면 Array, String, Map, TpyedArray 등에서 사용한다.

Array_console_dir

만약 빌트인 iterable이 아니라 본인이 구현한 함수 즉, 확장을 하고 싶다면 위에서 언급한 방법과 같이 확장하여 사용하면 된다. (참고, Symbol과 표준 빌트인 객체 확장)