1. Symbol 이란?
Symbol은 원시타입 데이터로, 유일무이한 값을 만드는데 사용된다. (Symbol의 뜻이 상징이라는 뜻이니까...)
그리고 이 값은 익명의 객체 속성(object property)을 만들 수 있는 특성을 가졌다. 한 마디로 객체의 키값은 문자열, 뿐만 아니라 Symbol형도 가능하다. Symbol() 을 사용하면 Symbol 값을 만들 수 있다.
// id는 새로운 심볼이 됩니다.
let id = Symbol();
아래와 같이 Symbol을 만들 때 인자를 줄 수 있는데, 이 인자는 Symbol을 나타내는 설명, 이름이다.
이 Symbol 이름은 어떠한 것에도 영향을 주지 않는 단순 '이름'역할만 한다. 디버깅시에 많이 쓰인다.
// 심볼 id에는 "id"라는 설명이 붙습니다.
let id = Symbol("id");
위에서도 말했듯이 Symbol은 고유한 값, 즉 유일무이한 값을 만들 때 사용한다. 그리고 Symbol 이름은 어떠한 것에도 영향을 끼치지 않는다. 따라서 다음 결과가 도출된다.
let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 === id2); // false
console.log(id1.description); // id
그리고 위와 같이 description으로 Symbol에 관한 설명을 얻을 수 있다.
2. Symbol 어디에 사용하지
Symbol은 계속 말하지만 유일무이한 값이다. 그리고 Symbol()에 전달되는 인자가 같다고 해도 Symbol은 생성할 때 마다 고유한 값이기 때문에 객체의 key로 쓰기에 알맞다. Symbol로 생성한 객체의 키는 다른 어떤 객체의 프로퍼티와도 충돌할 일이 없기 때문이다.
let key1 = Symbol('key');
let key2 = Symbol('key');
let obj = {};
obj[key1] = 'value1';
obj[key2] = 'value2';
console.log(obj);
객체에 동일한 key로 값을 초기화할 경우에는 그 key의 value는 계속 덮어씌워지게 된다. 따라서 보통의 경우라면 마지막에 값을 바꿔준 value2로 덮어씌워질 것이다. 하지만 위의 key들은 전부 Symbol형이다. 똑같은 형태로 선언을 해주었지만 Symbol형은 항상 유일무이한 값이기 때문에 obj은 아래와 같은 형태를 지닌다.
3. Symbol 사용법
Symbol()
위에서 봤듯이 Symbol()로 새로운 Symbol을 만들 수 있다. 하지만 이렇게 만든 Symbol은 또 다시 접근할 수가 없다.
let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 === id2); // false
console.log(id1.description); // id
위에 있던 예제인데 다시 한번 보자. 각각 동일한 description을 가진 Symbol을 생성했지만 서로 다른 Symbol이다.
Symbol.for()
위에서 봤듯이 그냥 Symbol(description)으로 사용하게 된다면 그냥 Symbol을 설명하는 description을 붙여서 만들 뿐이다. 하지만 우리는 우리가 선언한 Symbol에 대해서 접근하고 싶을수도 있다. 그럴때는 다음과 같이 Symbol.for(key)를 사용하면 된다.
// 전역 레지스트리에서 심볼을 읽습니다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만듭니다.
// 동일한 이름을 이용해 심볼을 다시 읽습니다(좀 더 멀리 떨어진 코드에서도 가능합니다).
let idAgain = Symbol.for("id");
// 두 심볼은 같습니다.
alert( id === idAgain ); // true
Symbol.for()과 Symbol()안에 들어가는 인자는 문자열로 같지만 전혀 다른 인자이다. Symbol.for()안에 들어가는 인자는 Symbol을 위한 key이고, Symbol()안에 들어가는 인자는 단지 Symbol을 설명할 description이다. 따라서 Symbol.for()로 선언한 Symbol은 나중에 접근이 가능하도록 전역 심볼 레지스트리에 저장이 된다.
let key1 = Symbol.for('key'); // 전역 레지스트리에 'key'라는 key를 가진 Symbol 생성
let key2 = Symbol.for('key'); // 전역 레지스트리에서 'key'라는 key를 가진 Symbol을 가져옴
let obj = {};
obj[key1] = 'value1';
console.log(key1 === key2) // true
console.log(obj[key2]); // value1
그냥 Symbol()을 사용하면 전역 레지스트리에 등록이 되지 않아서 나중에 접근할 수가 없지만 위와 같이 Symbol.for()을 사용한다면 나중에 접근하여서 똑같은 Symbol을 읽어와 사용할 수 있다.
Symbol.keyFor()
Symbol.key()는 전역 레지스트리에 등록된 Symbol을 key로 해당 Symbol을 찾아왔다.
하지만 Symbol.keyFor()은 인자로 Symbol을 넘겨주게 되면 전역 레지스트리에서 해당 Symbol의 key를 찾아온다.
// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
주의 할 것이 있다. 이 메소드는 반드시 전역 레지스트리에 등록된 Symbol을 넘겨주어야 한다.
전역 레지스트리에 등록되지 않은 Symbol이라면 undefined를 반환하게 된다.
전역 레지스트리에 등록되지 않은 Symbol이라 함은 Symbol()로 생성된 Symbol을 말한다. 이것은 인자로 넘겨주는것이 key값이 아닌 단지 해당 Symbol에 대한 description일 뿐이다. 따라서 이 description을 얻고 싶다면 다음과 같이 하면 된다.
let globalSymbol = Symbol.for("name"); // 전역 레지스트리에 name이라는 key를 가진 Symbol 등록
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name, 전역 심볼
alert( Symbol.keyFor(localSymbol) ); // undefined, 전역 심볼이 아님
alert( localSymbol.description ); // name
4. Symbol.iterator
Symbol에는 흔이 "well-known symbol"이라 불리는 몇가지 Symbol이 존재한다. 지금 설명할 Symbol.iterator도 그 중에 하나이다. 아래에 보이는 것들 중에서 length와 prototype을 제외한 것들이 well-known symbol이다.
이 중에서 Symbol.iterator에 대한 설명만 해보려고 한다.
iterable
iterable 객체는 배열을 일반화한 객체이다. 이 iterable 이라는 개념을 사용하면 어떤 객체든 for...of 사용할 수 있다. 배열은 대표적인 iterable 객체이고, 문자열 또한 그렇다. 이러한 iterable 객체는 Symbol.iterator를 key로 가진 프로퍼티를 가지고 있다.
아래는 console.dir(Array.prototype)을 한 결과이다. 맨 끝쪽에 가면 Symbol.iterator가 존재한다는 것을 알 수 있다.
아래는 console.dir(String.prototype)을 한 결과이다. 역시 맨 아래쪽에 존재한다.
이 이외에도 Map, Set, ... 등이 있다.
iterator
아래의 예시는 다음 글을 참조했다. https://poiemaweb.com/es6-symbol
// 이터러블
// Symbol.iterator를 프로퍼티 key로 사용한 메소드를 구현하여야 한다.
// 배열에는 Array.prototype[Symbol.iterator] 메소드가 구현되어 있다.
const iterable = ['a', 'b', 'c'];
// 이터레이터
// 이터러블의 Symbol.iterator를 프로퍼티 key로 사용한 메소드는 이터레이터를 반환한다.
const iterator = iterable[Symbol.iterator]();
// 이터레이터는 순회 가능한 자료 구조인 이터러블의 요소를 탐색하기 위한 포인터로서 value, done 프로퍼티를 갖는 객체를 반환하는 next() 함수를 메소드로 갖는 객체이다. 이터레이터의 next() 메소드를 통해 이터러블 객체를 순회할 수 있다.
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
위에서 iterable한 객체는 for...of를 사용할 수 있다고 했다. 위의 코드와 함께보면 아래 설명할 for...of의 동작순서, 원리에 대해서 조금은 도움이 될 것 같아서 가져왔다.
- for..of가 시작되자마자 for..of는 Symbol.iterator를 호출합니다(Symbol.iterator가 없으면 에러가 발생합니다). Symbol.iterator는 반드시 이터레이터(iterator, 메서드 next가 있는 객체) 를 반환해야 합니다.
- 이후 for..of는 반환된 객체(이터레이터)만을 대상으로 동작합니다.
- for..of에 다음 값이 필요하면, for..of는 이터레이터의 next()메서드를 호출합니다.
- next()의 반환 값은 {done: Boolean, value: any}와 같은 형태이어야 합니다. done=true는 반복이 종료되었음을 의미합니다. done=false일땐 value에 다음 값이 저장됩니다.
결국 for...of는 iterator를 호출해서 done: true가 나올 때 까지 next()를 호출해서 그 다음 value 값을 계속해서 가져오는 것이다.
'Web > Vanilla JS' 카테고리의 다른 글
일급객체, 일급함수 (0) | 2023.09.03 |
---|---|
호이스팅(Hoisting) (2) | 2023.09.03 |
얕은 복사와 깊은 복사(Shallow Copy & Deep Copy) (0) | 2023.04.03 |
Object Method (0) | 2023.03.30 |