📢 22/05/26 복습
javascript.info, https://ko.javascript.info/symbol
simon's study, https://velog.io/@simoniful/JS-13-Symbol-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0
참고 사이트에 내용을 개인적으로 복습하기 편하도록 재구성한 글입니다.
자세한 설명은 참고 사이트를 살펴보시기 바랍니다.
"심볼(symbol)"은 유일한 식별자(unique identifier)를 만들고 싶을 때 사용합니다.
Symbol()
을 사용하면 심볼값을 만들 수 있습니다.
// id는 새로운 심볼이 됩니다.
let id = Symbol();
심볼을 만들 때 심볼 이름이라 불리는 설명을 붙일 수도 있습니다. 심볼 이름은 디버깅 시 아주 유용합니다.
// 심볼 id에는 "id"라는 설명이 붙습니다.
let id = Symbol("id");
심볼은 유일성이 보장되는 자료형이기 때문에, 설명이 동일한 심볼을 여러 개 만들어도 각 심볼값은 다릅니다. 심볼에 붙이는 설명(심볼 이름)은 어떤 것에도 영향을 주지 않는 이름표 역할만을 합니다.
설명이 같은 심볼 두 개를 만들고 이를 비교해보겠습니다. 동일 연산자(==
)로 비교 시 false
가 반환되는 것을 확인할 수 있습니다.
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
🔥 심볼은 문자형으로 자동 형 변환되지 않습니다.
자바스크립트에선 문자형으로의 암시적 형 변환이 비교적 자유롭게 일어나는 편입니다. alert
함수가 거의 모든 값을 인자로 받을 수 있는 이유가 이 때문이죠. 그러나 심볼은 예외입니다. 심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않습니다.
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
문자열과 심볼은 근본이 다르기 때문에 우연히라도 서로의 타입으로 변환돼선 안 됩니다. 자바스크립트에선 "언어 차원의 보호장치(language guard)"를 마련해 심볼형이 다른 형으로 변환되지 않게 막아줍니다.
심볼을 반드시 출력해줘야 하는 상황이라면 아래와 같이 .toString()
메서드를 명시적으로 호출해주면 됩니다.
let id = Symbol("id");
alert(id.toString()); // Symbol(id)가 얼럿 창에 출력됨
symbol.description
프로퍼티를 이용하면 설명만 보여주는 것도 가능합니다.
let id = Symbol("id");
alert(id.description); // id
심볼을 이용하면 "숨김(hidden)" 프로퍼티를 만들 수 있습니다. 숨김 프로퍼티는 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티입니다.
서드파티 코드에서 가지고 온 user
라는 객체가 여러 개 있고, user
를 이용해 어떤 작업을 해야 하는 상황이라고 가정해 봅시다. user
에 식별자를 붙여주도록 합시다.
식별자는 심볼을 이용해 만들도록 하겠습니다.
let user = { // 서드파티 코드에서 가져온 객체
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근할 수 있습니다.
그런데 문자열 "id"
를 키로 사용해도 되는데 Symbol("id")
을 사용한 이유가 무엇일까요?
user
는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없습니다. 그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user
에 식별자를 부여할 수 있습니다.
상황 하나를 더 가정해보겠습니다. 제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 값"
// 의도치 않게 값이 덮어 쓰여서 우리가 만든 식별자는 무의미해집니다.
객체 리터럴 {...}
을 사용해 객체를 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 합니다.
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)
에서도 키가 심볼인 프로퍼티는 배제됩니다. "심볼형 프로퍼티 숨기기(hiding symbolic property)"라 불리는 이런 원칙 덕분에 외부 스크립트나 라이브러리는 심볼형 키를 가진 프로퍼티에 접근하지 못합니다.
그런데 💥 Object.assign
은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사합니다.
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
뭔가 모순이 있는 것 같아 보이지만, 이는 의도적으로 설계된 것입니다. 객체를 복사하거나 병합할 때, 대개 id
같은 심볼을 포함한 프로퍼티 전부를 사용하고 싶어 할 것이라는 생각에서 이렇게 설계되었습니다.
앞서 살펴본 것처럼, 심볼은 이름이 같더라도 모두 별개로 취급됩니다. 그런데 이름이 같은 심볼이 같은 개체를 가리키길 원하는 경우도 가끔 있습니다. 애플리케이션 곳곳에서 심볼 "id"
를 이용해 특정 프로퍼티에 접근해야 한다고 가정해 봅시다.
전역 심볼 레지스트리(global symbol registry) 는 이런 경우를 위해 만들어졌습니다. 전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해줍니다.
레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)
를 사용하면 됩니다.
이 메서드를 호출하면 이름이 key
인 심볼을 반환합니다. 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)
을 만들고 레지스트리 안에 저장합니다.
// 전역 레지스트리에서 심볼을 읽습니다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만듭니다.
// 동일한 이름을 이용해 심볼을 다시 읽습니다(좀 더 멀리 떨어진 코드에서도 가능합니다).
let idAgain = Symbol.for("id");
// 두 심볼은 같습니다.
alert( id === idAgain ); // true
전역 심볼 레지스트리 안에 있는 심볼은 전역 심볼이라고 불립니다. 애플리케이션에서 광범위하게 사용해야 하는 심볼이라면 전역 심볼을 사용하세요.
전역 심볼을 찾을 때 사용되는 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)"은 자바스크립트 내부에서 사용되는 심볼입니다. 시스템 심볼을 활용하면 객체를 미세 조정할 수 있습니다.
명세서 내의 표, 잘 알려진 심볼(well-known symbols)에서 어떤 시스템 심볼이 있는지 살펴보세요.
📌 Symbol.hasInstance
📌 Symbol.isConcatSpreadable
📌 Symbol.iterator
📌 Symbol.toPrimitive
📌 ...
Well Known Symbol는 알고리즘에 이름을 부여하고 이름을 통해 참조하기 위한 시스템 심볼 값을 의미합니다.
@@
는 Well Known Symbol을 나타내는 기호입니다. @@match
와 Symbol.match
는 같은 형태이며 스펙에서는 @@match
형태를, 개발자는 Symbol.match
형태를 사용합니다.
match()
메소드를 실행하면 디폴트로 @@match
가 실행됩니다. 소스 코드에 Symbol.match
를 작성하면 @@match
가 실행되지 않고, Symbol.match
가 우선 실행됩니다.
예를 들어 String.prototype.match
가 호출되면 우선 개발자 코드에서 Symbol.match
를 찾습니다. 성공한다면 해당 함수를 실행합니다. 하지만, 찾지 못했을 경우 디폴트인 @@match
를 실행합니다.
원래, 기존에는 Symbol.XXXX
가 따로 제공되지 않았기에 바로 엔진에서 @@XXXX
알고리즘이 호출되어 실행되었습니다. 하지만 이제는 개발자가 운용(변경, 추가)할 수 있도록 오픈되었습니다. 개발자 코드로 디폴트 기능에 유용성을 가할 수 있도록 오버라이딩 할 수 있습니다.
여기서 Symbol
항목이 개발자가 작성하는 코드들입니다. 대응 부분의 함수 및 알고리즘을 실행하면 우선 Symbol
항목에 관련 내용이 오버라이딩 되어있으면 그 코드를 실행하고, 없으면 엔진의 기본 알고리즘을 호출합니다.