자바스크립트의 데이터 타입에는 크게 2가지로 기본형(원시형), 참조형이 있습니다. 기본형에는 number, string, boolean, null, undefined 가 있으며 ES2015(ES6)에 추가된 Symbol이 있습니다. Symbol은 무엇이며 언제 사용하고 왜 추가된 걸까요?
Symbol이란
자바스크립트의 객체 타입은 순서가 없는 프로퍼티 집합으로 ES6 이전까지는 프로퍼티명이 오로지 문자열이었지만 ES6 이후에는 심벌 타입도 프로퍼티명으로 지정할 수 있게 되었습니다.
객체는 순서가 없는 프로퍼티 모음이라 순서가 보장되지 않지만 어떤 규칙이 있는 것 같아서 찾아봤습니다. (관련 링크) ES2015 기준으로 프로퍼티 순서의 규칙은 아래와 같이 정의됩니다. 1. 오름차순의 정수 2. 삽입 순서순의 문자열 3. 삽입 순서순의 Symbol 위 순서는 ES2015 기준으로, 일부 구형 브라우저는 이를 보장하지 않았지만 ES2015 기준 아래의 메서드들이 이 순서를 보장해줬습니다.
심벌값은 암묵적으로 문자열이나 숫자 타입으로 변환하지 않습니다. 하지만 불리언 타입으로는 암묵적으로 타입 변환이 이뤄집니다.
const uniqueSymbol = Symbol();
console.log(uniqueSymbol + ""); // Cannot convert a Symbol value to a string
console.log(+uniqueSymbol); // Cannot convert a Symbol value to a number
console.log(!!uniqueSymbol); // true
if (uniqueSymbol) console.log("uniqueSymbol is not empty"); // true
또한 심벌 값도 문자열, 숫자, 불리언과 같이 객체처럼 접근시 암묵적으로 래퍼 객체를 생성합니다. description 프로퍼티, toString 메서드는 Symbol.prototype의 프로퍼티입니다.
원시값의 경우, 객체처럼 마침표 표기법(혹은 대괄호 표기법)으로 접근시 JS 엔진이 일시적으로 원시값을 연관된 객체로 변환합니다. 그리고 변환된 객체로 프로퍼티 접근 및 메서드 호출 후 원시값으로 되돌립니다.
위 과정을 자세히 설명하면 다음과 같습니다. 1. 위 예시를 기준으로,String 빌트인 생성자 함수를 이용해 인스턴스를 생성합니다. 2. String 생성자 함수를 이용해 생성된 인스턴스의 내부 슬롯인 [[StringData]]에 원시값이 할당되고 해당 인스턴스는 String.prototype의 메서드를 상속받아 사용합니다. 3. 래퍼 객체 처리 종료시, 식별자를 [[StringData]] 내부 슬롯에 할당된 원시값으로 되돌리고 래퍼 객체는 가비지 컬렉션의 대상이 됩니다.
즉, 래퍼 객체란 문자열, 숫자, 불리언 값에 대해 객체처럼 접근시 생성되는 임시 객체를 의미하며 이때 래퍼 객체를 생성하기 위해 String, Number, Boolean과 같은 빌트인 생성자 함수가 필요합니다.
전역 심볼 레지스트리
다른 코드에서도 쓸 수 있도록 심벌 정의 및 공유하기 위해 Symbol.for(), Symbol.keyFor()를 이용해 전역 심벌 레지스트리 정의할 수 있습니다. (Symbol.iterator 매커니즘과 같은 일종의 확장)
해당 메서드들을 이용해 전역 심벌 레지스트리 테이블과 런타임 환경 사이에서 심벌 값을 전해주는 역할을 합니다. 전역 심벌 레지스트리는 대부분 자바스크립트 컴파일 인프라에 내장되어 있고, 레지스트리 내용은 자바스크립트 런타임 환경에서 위 메서드를 사용하지 않는 이상 접근이 불가능합니다.
const a = Symbol.for("shared");
const b = Symbol.for("shared");
console.log(a === b); // true
console.log(a.toString()); // Symbol(shared)
console.log(Symbol.keyFor(b)); // shared
Symbol Use Case
프로퍼티 은닉
내 코드에서만 쓸 수 있도록 비공개로 둬 다른 코드의 프로퍼티와 충돌 방지
for..in이나 Object.keys, Object.getOwnPropertyNames 메서드로 심벌 키를 찾을 수 없기에 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있음
단, ES2015에 도입된 Object.getOwnPropertySymbols 메서드 사용시 심볼 값을 프로퍼티 키로 사용해 생성한 프로퍼티를 찾을 수 있음
값 변경 및 중복 방지 (JS에서 enum 사용하기)
상수 중에 값에는 특별한 의미가 없고 상수 이름 자체에 의미가 있는 경우, 상수값 변경 및 다른 변수값과 중복 방지를 위해 유일뮤이한 심벌값 사용 (아래 예시에서는 자바스크립트에서 enum을 따라하기 위해 객체 동결을 위한 Object.freeze와 심벌을 사용)
// case
const Direction = {
UP: 1,
DOWN: 2,
LEFT: 3,
RIGHT: 4,
};
// better case
const Direction = Object.freeze({
UP: Symbol('up'),
DOWN: Symbol('down'),
LEFT: Symbol('left'),
RIGHT: Symbol('right'),
})
심볼과 표준 빌트인 객체 확장
빌트인 객체에 사용자 정의 메서드 추가시, ECMAScript에 중복 메서드명이 존재하면 문제가 될 수 있기에, 중복 가능성이 없는 심벌 값으로 빌트인 객체 확장시 충돌 위험이 없어져 안전한 빌트인 객체 확장 가능
1. 표준 빌트인 객체 ECMAScript 명세에 정의된 객체로, 애플리케이션 전역의 공통 기능을 제공하며 실행 환경(브라우저 혹은 Node.js)에 상관 없이 언제나 사용할 수 있습니다. (String, Number, Object 등)
2. 호스트 객체 JS 실행환경에서 추가로 제공하는 객체로, 브라우저 환경에서는 window, DOM 등이 있으며 Node.js에서는 Node.js의 고유한 API를 호스트 객체로 제공합니다.
3. 사용자 정의 객체 사용자가 직접 정의한 객체를 뜻합니다.
Well-known Symbol
Symbol 함수의 프로퍼티에 할당된 빌트인 심벌값을 ECMAScript 사양에서는 Well-Known Symbol이라고 하며 JS 엔진의 내부 알고리즘에 사용됩니다.
Array, String, Map 등과 같이 for...of문으로 순회 가능한 빌트인 이터러블은 Well-Known Symbol인 Symbol.iterator를 키로 갖는 메서드를 가지며 이때 해당 메서드 호출시 이터레이터를 반환합니다. 즉, 빌트인 이터러블은 이터레이션 프로토콜을 준수합니다.
일반 객체를 이터러블처럼 동작하도록 구현할 경우, 이터레이션 프로토콜을 따르기 위해 Symbol.iterator를 키로 갖는 메서드를 객체에 추가하고 이터레이터를 반환하도록 구현하면 됩니다. 이때, 일반 객체에 추가해야 하는 메서드의 키인 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
}