심볼형

자바스크립트는 객체 프로퍼티 키로 오직 문자형과 심볼형만을 허용한다.

심볼

심볼은 고유하고 변경 불가능한(immutable) 값을 나타내며, 주로 객체의 속성 키로 사용되는 경우가 많다.
심볼은 다음과 같은 특징을 갖고 있다.

  1. 고유성(Uniqueness): 심볼은 항상 고유하다. 동일한 설명 문자열을 가진 두 개의 심볼도 서로 다르다.
     let id1 = Symbol("id");
     let id2 = Symbol("id");
    
     alert(id1 == id2); // false
    
     const symbol1 = Symbol("description");
     const symbol2 = Symbol("description");
    
     console.log(symbol1 === symbol2); // false
    
  2. 변경 불가능성(Immutable): 심볼은 한 번 생성되면 그 값을 변경할 수 없다. 즉, 심볼 값을 수정하거나 재할당할 수 없다.
  3. 속성 키로 사용: 주로 객체의 속성 키로 사용된다. 심볼은 다른 속성과 충돌하지 않고, 원래 속성 이름과 함께 사용하여 객체의 속성을 숨길 수 있다.
  4. 전역 심볼(Symbol.for): 전역 심볼은 전역 심볼 레지스트리에 저장되며 전역에서 공유된다. 동일한 이름의 전역 심볼은 항상 동일한 심볼 값을 반환한다.
  5. 심볼 속성 접근: 심볼로 정의된 속성은 일반적인 점 표기법(.)으로는 접근할 수 없으며, 대괄호 표기법([])과 Object.getOwnPropertySymbols()를 사용하여 접근한다.

‘숨김’ 프로퍼티

심볼을 이용하면 ‘숨김(hidden)’ 프로퍼티를 만들 수 있다. 숨김 프로퍼티는 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티다.

let user = { // 서드파티 코드에서 가져온 객체
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근할 수 있습니다.

user는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없다.
그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를 부여할 수 있다.

서드파티 코드(Third-party code) : 외부 라이브러리 및 플러그인, 외부 API, 외부 스크립트 및 자원을 가르킨다.

제3의 스크립트(자바스크립트 라이브러리 등)에서 user를 식별해야 하는 상황이라고 가정
user의 원천인 서드파티 코드, 현재 작성 중인 스크립트, 제3의 스크립트가 각자 서로의 코드도 모른 채 user를 식별해야 하는 상황이다.

제 3의 스크립트에선 아래와 같이 Symbol(“id”)을 이용해 전용 식별자를 만들어 사용할 수 있다.

// ...
let id = Symbol("id");

user[id] = "제3 스크립트 id 값";

심볼은 유일성이 보장되므로 이름이 같더라도 우리가 만든 식별자와 제3의 스크립트에서 만든 식별자가 충돌하지 않는다.

만약 심볼 대신 문자열 “id”를 사용해 식별자를 만들었다면 충돌이 발생할 가능성이 있다.

let user = { name: "John" };

// 문자열 "id"를 사용해 식별자를 만든다.
user.id = "스크립트 id 값";

// 만약 제3의 스크립트가 우리 스크립트와 동일하게 문자열 "id"를 이용해 식별자를 만들었다면...

user.id = "제3 스크립트 id 값"
// 의도치 않게 값이 덮어 쓰여서 우리가 만든 식별자는 무의미해진다.


Symbols in a literal

객체 리터럴 {…}을 사용해 객체를 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 한다.

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // "id": 123은 안됨
};

“id: 123”이라고 하면, 심볼 id가 아니라 문자열 “id”가 키가 된다.


심볼은 for…in 에서 배제된다.

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name과 age만 출력되고, 심볼은 출력되지 않습니다.

// 심볼로 직접 접근하면 잘 작동합니다.
alert( "직접 접근한 값: " + user[id] );

Object.keys(user) //(user라는 변수에 할당된 객체)의 열거 가능한 속성(키)들을 배열로 반환
에서도 키가 심볼인 프로퍼티는 배제된다.

Object.assign은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사한다.

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123


전역 심볼

전역 심볼 레지스트리 안에 있는 심볼은 전역 심볼이라고 불린다.
애플리케이션에서 광범위하게 사용해야 할때 사용한다.

전역 심볼 레지스트리는 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해준다.

레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)를 사용하면된다.

이 메서드를 호출하면 이름이 key인 심볼을 반환한다. 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)을 만들고 레지스트리 안에 저장한다.

// 전역 레지스트리에서 심볼을 읽는다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만든다.

// 동일한 이름을 이용해 심볼을 다시 읽는다(좀 더 멀리 떨어진 코드에서도 가능하다).
let idAgain = Symbol.for("id");

// 두 심볼은 같다.
alert( id === idAgain ); // true


Symbol.keyFor

전역 심볼을 찾을 때 사용되는 Symbol.for(key)에 반대되는 메서드다. Symbol.keyFor(sym)를 사용하면 이름을 얻을 수 있다.

// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor는 전역 심볼 레지스트리를 뒤져서 해당 심볼의 이름을 얻어낸다. 검색 범위가 전역 심볼 레지스트리이기 때문에 전역 심볼이 아닌 심볼에는 사용할 수 없다.
전역 심볼이 아닌 인자가 넘어오면 Symbol.keyFor는 undefined를 반환한다.

전역 심볼이 아닌 모든 심볼은 description 프로퍼티가 있다.
일반 심볼에서 이름을 얻고 싶으면 description 프로퍼티를 사용하면 된다.

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name, 전역 심볼
alert( Symbol.keyFor(localSymbol) ); // undefined, 전역 심볼이 아님

alert( localSymbol.description ); // name


시스템 심볼

‘시스템 심볼(system symbol)’은 자바스크립트 내부에서 사용되는 심볼이다.
시스템 심볼은 Symbol 객체의 정적 속성으로 제공되며, 다음과 같은 몇 가지 내장 시스템 심볼이 있다.

  1. Symbol.iterator: 이 심볼은 객체를 반복(iterate)할 수 있는 방법을 나타내는데, 주로 for…of 루프와 같은 반복문에서 사용된다. 객체가 이 심볼을 가지고 있으면 해당 객체를 순회할 수 있다.
  2. Symbol.toPrimitive: 이 심볼은 객체를 원시 값(primitive value)으로 변환하는 알고리즘에 사용된다. 예를 들어, valueOf()와 toString() 메서드를 통해 객체를 원시 값으로 변환하는데 활용된다.
  3. Symbol.species: 이 심볼은 생성자 함수에서 파생된 객체를 만들 때 생성자 함수 자체를 참조하기 위해 사용된다. 특히, 하위 클래스가 Array와 같은 내장 클래스를 확장할 때 유용하다.
  4. Symbol.hasInstance: 이 심볼은 객체가 특정 생성자 함수의 인스턴스인지를 판별하기 위해 사용된다. instanceof 연산자와 관련이 있으며, 사용자 정의 객체의 instanceof 동작을 커스터마이징하는 데 활용할 수 있다.
  5. Symbol.match, Symbol.replace, Symbol.search, Symbol.split: 이러한 심볼은 문자열 메서드(String.prototype)에서 정규 표현식을 사용하여 문자열을 조작할 때 활용된다.



객체를 원시형으로 변환하기

obj1 + obj2 처럼 객체끼리 더하는 연산을 하거나 obj1 - obj2 처럼 객체끼리 빼는 연산을 하면 자동 형 변환이 일어난다. 객체는 원시값으로 변환되고, 그 후 의도한 연산이 수행된다.

  1. 객체는 논리 평가 시 true를 반환한다.
     onst myObject = { key: 'value' };
    
     if (myObject) {
     console.log('객체는 true로 평가된다.');
     } else {
     console.log('객체는 false로 평가되지 않는다.');
     }
    
  2. 숫자형으로의 형 변환은 객체끼리 빼는 연산을 할 때나 수학 관련 함수를 적용할 때 일어난다.
  3. 문자형으로의 형 변환은 대개 alert(obj)같이 객체를 출력하려고 할 때 일어난다.


ToPrimitive

특수 객체 메서드를 사용하면 숫자형이나 문자형으로의 형 변환을 원하는 대로 조절할 수 있다.
객체 형 변환은 세 종류로 구분되는데, ‘hint’라 불리는 값이 구분 기준이 된다.
‘hint’는 ‘목표로 하는 자료형’ 정도로 이해하자.

“string”

alert 함수같이 문자열을 기대하는 연산을 수행할 때는(객체-문자형 변환), hint가 string이 된다.

// 객체를 출력하려고 함
alert(obj);

// 객체를 프로퍼티 키로 사용하고 있음
anotherObj[obj] = 123;

“number”

수학 연산을 적용하려 할 때(객체-숫자형 변환), hint는 number가 된다.

// 명시적 형 변환
let num = Number(obj);

// (이항 덧셈 연산을 제외한) 수학 연산
let n = +obj; // 단항 덧셈 연산
let delta = date1 - date2;

// 크고 작음 비교하기
let greater = user1 > user2;

“default”

연산자가 기대하는 자료형이 ‘확실치 않을 때’ hint는 default가 된다. 아주 드물게 발생한다.

이항 덧셈 연산자 +는 피연산자의 자료형에 따라 문자열을 합치는 연산을 할 수도 있고 숫자를 더해주는 연산을 할 수도 있다. 따라서 +의 인수가 객체일 때는 hint가 default가 된다.

동등 연산자 ==를 사용해 객체-문자형, 객체-숫자형, 객체-심볼형끼리 비교할 때도, 객체를 어떤 자료형으로 바꿔야 할지 확신이 안 서므로 hint는 default가 된다.

// 이항 덧셈 연산은 hint로 `default`를 사용합니다.
let total = obj1 + obj2;

// obj == number 연산은 hint로 `default`를 사용합니다.
if (user == 1) { ... };

크고 작음을 비교할 때 쓰이는 연산자 <, > 역시 피연산자에 문자형과 숫자형 둘 다를 허용하는데, 이 연산자들은 hint를 ‘number’로 고정한다.

자바스크립트는 형 변환이 필요할 때, 아래와 같은 알고리즘에 따라 원하는 메서드를 찾고 호출한다.

  1. 객체에 objSymbol.toPrimitive메서드가 있는지 찾고, 있다면 메서드를 호출한다.
    Symbol.toPrimitive는 시스템 심볼로, 심볼형 키로 사용된다.
  2. 1에 해당하지 않고 hint가 “string”이라면,
    • obj.toString()이나 obj.valueOf()를 호출한다(존재하는 메서드만 실행됨).
  3. 1과 2에 해당하지 않고, hint가 “number”나 “default”라면
    • obj.valueOf()나 obj.toString()을 호출한다(존재하는 메서드만 실행됨).


Symbol.toPrimitive

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 데모:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

이렇게 메서드를 구현해 놓으면 user는 hint에 따라 (자기 자신을 설명해주는) 문자열로 변환되기도 하고 (가지고 있는 돈의 액수를 나타내는) 숫자로 변환되기도 한다. user[Symbol.toPrimitive]를 사용하면 메서드 하나로 모든 종류의 형 변환을 다룰 수 있다.

추가 형 변환

곱셈을 해주는 연산자 *는 피연산자를 숫자형으로 변환시키다.
객체가 피연산자일 때는 다음과 같은 단계를 거쳐 형 변환이 일어난다.

  1. 객체는 원시형으로 변화된다.
  2. 변환 후 원시값이 원하는 형이 아닌 경우 또다시 형 변환이 일어난다.
let obj = {
  // 다른 메서드가 없으면 toString에서 모든 형 변환을 처리한다.
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4, 객체가 문자열 "2"로 바뀌고, 곱셈 연산 과정에서 문자열 "2"는 숫자 2로 변경된다.

이항 덧셈 연산은 위와 같은 상황에서 문자열을 연결한다.

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // 22("2" + 2), 문자열이 반환되기 때문에 문자열끼리의 병합이 일어났다.

참고문서

https://ko.javascript.info/object-basics