심볼형

RHUK2·2021년 6월 21일
0

Javascript

목록 보기
16/56

📢 22/05/26 복습


📚 Reference


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 값"
// 의도치 않게 값이 덮어 쓰여서 우리가 만든 식별자는 무의미해집니다.

Symbols in a literal

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

let id = Symbol("id");

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

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

심볼은 for...in 에서 배제됩니다

💥 키가 심볼인 프로퍼티는 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.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.keyForundefined를 반환합니다.

전역 심볼이 아닌 모든 심볼은 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는 알고리즘에 이름을 부여하고 이름을 통해 참조하기 위한 시스템 심볼 값을 의미합니다.

@@는 Well Known Symbol을 나타내는 기호입니다. @@matchSymbol.match는 같은 형태이며 스펙에서는 @@match 형태를, 개발자는 Symbol.match 형태를 사용합니다.

match() 메소드를 실행하면 디폴트로 @@match가 실행됩니다. 소스 코드에 Symbol.match를 작성하면 @@match가 실행되지 않고, Symbol.match가 우선 실행됩니다.

예를 들어 String.prototype.match가 호출되면 우선 개발자 코드에서 Symbol.match를 찾습니다. 성공한다면 해당 함수를 실행합니다. 하지만, 찾지 못했을 경우 디폴트인 @@match를 실행합니다.

원래, 기존에는 Symbol.XXXX가 따로 제공되지 않았기에 바로 엔진에서 @@XXXX 알고리즘이 호출되어 실행되었습니다. 하지만 이제는 개발자가 운용(변경, 추가)할 수 있도록 오픈되었습니다. 개발자 코드로 디폴트 기능에 유용성을 가할 수 있도록 오버라이딩 할 수 있습니다.

여기서 Symbol 항목이 개발자가 작성하는 코드들입니다. 대응 부분의 함수 및 알고리즘을 실행하면 우선 Symbol 항목에 관련 내용이 오버라이딩 되어있으면 그 코드를 실행하고, 없으면 엔진의 기본 알고리즘을 호출합니다.

profile
생각 많이 하지 않기 😎

0개의 댓글