치춘짱베리굿나이스

자바스크립트에서의 Symbol 본문

Javascript + Typescript/이론과 문법

자바스크립트에서의 Symbol

치춘 2022. 8. 20. 01:50

Symbol

뭐 하는 녀석인지?

태초에 자바스크립트는 원시 자료형 5개와 객체 자료형 1개 총 6개의 자료형으로 이루어져 있었다

Number, String, Boolean, null, undefined 그리고 객체 Object가 그것이었는데, ES6 (2015년) 에 원시자료형팀 환상의 식스맨으로 Symbol이 합류했다

심볼은 객체의 고유 식별자로 활용할 수 있는 원시자료형으로, 쉽게 말해 객체 내부 프로퍼티의 키를 설정할 때 사용할 수 있다

심볼을 사용하면 프로퍼티 키가 겹치지 않고 고유한 값으로 설정되므로, 키가 중복으로 설정됨으로써 발생하는 충돌을 막을 수 있다

프론트엔드에서 클래스명을 겹치지 않게 설정하기 위해 CSS module을 사용하거나, CSS-in-JS 라이브러리들 (Styled Components, Emotion) 등을 사용하는 이유와 같다고 보면 된다

Symbol 선언 및 초기화하기

선언 및 초기화

const newSymbol = Symbol();

간단하다

생성자 쓰듯이 Symbol(); 함수를 호출하면 새로운 심볼이 생성된다

 

const newSymbol = new Symbol();
// TypeError 발생

Symbol() 함수는 생성자가 아닌 단순 함수이므로, new를 붙여 호출하면 타입 에러가 발생한다

 

const foo = Symbol('symbol');

함수의 인자로 문자열이나 숫자를 넣을 수 있으며, 이는 내부적으로 디버깅 시에 심볼 식별용으로 사용된다

 

내부적으론 description이라고 부른다

말 그대로 이 심볼을 설명해줄 수 있는 요소라고 보면 된다

실제로 저 값이 심볼 생성에 영향을 미치진 않는다

전역으로 선언하기 (전역 심볼 레지스트리에 등록하기)

const foo = Symbol.for("chichoon");

Symbol.for 메서드를 사용하면 전역 레지스트리에 심볼을 등록하거나, 찾아올 수 있다

인자로 키를 하나 받으며, 이 키에 대한 심볼이 전역 레지스트리에 등록되어 있을 경우 이를 반환하고, 없으면 새로 생성한다

 

function foo() {
  const sym = Symbol.for("foo");
  return sym;
}

function test() {
  const symbolA = foo();
  const symbolB = Symbol.for("foo");
  console.log(symbolA === symbolB);
}

test();

테스트를 위해 함수 스코프를 아예 분리해 보았다

Symbol.for(”foo”)foo 함수 내에서 호출되었으므로, 다른 함수 test에서 호출한 Symbol.for(”foo”) 는 다른 값을 가져야 할 것 같다

 

하지만 foo에서 Symbol.for(”foo”) 로 생성한 symbolA 와, test에서 Symbol.for(”foo”) 로 생성한 symbolB를 비교하면 같은 값이라고 나온다

이는 foo 함수 내에서 Symbol.for(”foo”) 를 호출함으로써 심볼이 전역 심볼 레지스트리에 등록되었기 때문으로, 인자로 넣어준 “foo” 는 전역 심볼 레지스트리에서 이 심볼을 찾기 위한 식별자 (key) 로 사용된다

test 함수 내에서 호출된 Symbol.for(”foo”) 는, foo 함수에 의해 심볼 레지스트리에 이미 “foo” 라는 식별자를 가진 심볼이 등록되었으므로 이를 그대로 반환하고, 따라서 두 값 (symbolA, symbolB) 은 완벽히 같은 값을 가리키게 된다

 

console.log(Symbol.keyFor(symbolA));

이렇게 전역 레지스트리에 등록한 심볼은 keyFor 메서드를 이용해서 식별자를 다시 가져올 수 있다

특이점

console.log(typeof symbol1);

typeof“symbol”을 반환한다

고유한 하나의 자료형이라 그렇다

 

const symbol1 = Symbol("symbol");
const symbol2 = Symbol("symbol");
console.log(symbol1 === symbol2);

위에 적었듯 description은 단순히 디버깅 시 설명용으로만 사용되므로, 같은 인자를 넘겨줬다고 해서 같은 값이 되진 않는다

symbol1symbol2는 같은 description (”symbol”) 을 인자로 넘겨받은 심볼이지만, 비교연산을 사용해보면 둘이 다른 값이라고 출력된다

전역 레지스트리에서 같은 값을 참조하지 않는 한, 모든 심볼은 서로 다른 고유의 값을 갖는다

선언한 Symbol 사용해보기

용례: 객체의 key로 사용하기

const obj = {};
const symbolA = Symbol("symbolA");
const symbolB = Symbol("symbolB");
obj[symbolA] = "hello";
obj[symbolB] = "world";

console.log(obj);

실제로 객체의 key로 응용해 보았다

출력해보면 객체의 키에는 [Symbol(<description으로 넘겨준 인자>)]가 들어가고 있다

 

const obj = {};
const symbolA = Symbol();
const symbolB = Symbol();
obj[symbolA] = "hello";
obj[symbolB] = "world";

console.log(obj);

description에 아무런 인자를 넣지 않아도 고유한 값으로 인식되어 들어간다

출력할 때만 똑같이 보일 뿐..

이처럼 description에 무슨 값이 들어가든 Symbol 변수는 객체의 키로 활용될 때 절대 고유한 유일의 값으로 사용된다는 것을 알 수 있다

거대한 객체를 다룰 때 객체의 key를 나도 모르게 겹치는 것으로 설정해버려서 기존 값을 덮어씌워버리는 대참사를 막기 좋다

다만 심볼은 매번 생성할 때마다 달라지므로, 키를 나중에 또 쓰고 싶다면 잘 보관하거나 전역 레지스트리에 보관해야 하것다

내장-미리된 심볼

시간 절약! 코드도 깔끔 즐겁다

위처럼 우리가 직접 선언해서 사용하는 심볼 말고도, 자바스크립트 엔진 자체에 미리 내장되어 있는 심볼들이 존재한다

Built-in Symbol, 또는 Well-Known Symbol이라고 하며, 순회나 정규식 사용 등의 기능을 제공한다

Symbol.iterator

제일 유명한 내장 심볼이 Symbol.iterator일 것이다

Symbol.iterator은 특정 객체를 순회가능하게 (Iterable) 만들어준다

순회가능하다는 것은, for … of 문에서 객체를 사용할 수 있다는 것이다 (댑악)

const obj = {
  a: "hello",
  b: "world",
  c: "this",
  d: "is",
  e: "chichoon",
};
const str = "";
for (let value of obj) str += value + " ";
console.log(str);

원래 이렇게 TypeError가 발생하면서 순회를 할 수 없었다

이 객체를 순회가능한 객체로 만들어 for문으로도 값에 접근할 수 있도록 해보자

 

const obj = {
  a: "hello",
  b: "world",
  c: "this",
  d: "is",
  e: "chichoon",
  [Symbol.iterator]: function () {
    let values = Object.values(this);
    let index = 0;
    return {
      next: () => ({
        value: values[index++],
        done: index > values.length,
      }),
    };
  },
};

객체의 내부에 [Symbol.iterator] 을 키로 갖는 프로퍼티를 추가한다

이 프로퍼티는 함수 형태로, valuedone을 반환하고 인자 없는 함수이다

앞서 적었듯 Symbol은 객체의 고유한 키로 사용되는 자료형이므로, Symbol.iterator도 고유한 키로 사용될 수 있다

 

next: () => ({
  value: values[index++],
  done: index > values.length,
}),

Symbol.iterator 프로퍼티는 next라는 내부 함수를 반환하는 함수이다

next 함수는 아무런 인자를 받지 않고 단지 value, done이라는 프로퍼티를 가진 객체를 반환하는데,

  • value: 현재 객체를 for문 등으로 순회할 때 반환할 값
  • done: 순회 종료 조건

 

해당 객체는 순회할 때 필요한 요소들을 담고 있다

코드를 천천히 뜯어보면,

  1. values는 본 객체 (obj) 의 값들의 배열이다 (Object.values 메서드로 가져온 값 배열)
  2. index는 0부터 시작하며, Symbol.iterator 메서드의 스코프 내에서 1씩 증가한다
  3. Symbol.iterator 메서드는 next라는 함수를 반환하고 종료된다. 이때 next 함수는 클로저로, Symbol.iterator 메서드의 실행이 끝나더라도 자신이 속한 렉시컬 스코프 (Symbol.iterator 메서드의 스코프) 내의 변수들인 values, index를 참조할 수 있다
  4. 따라서 next 함수를 호출할 때마다 Symbol.iterator 스코프에 있는 index 변수가 1씩 증가한다
  5. index 변수가 후위연산자에 따라 1씩 증가하면, value 또한 인덱스가 증가하면서 다음 값을 가리키게 된다
  6. index가 계속 증가하면서 다음 value를 가리키다가, 특정 시점이 되면 done 조건을 만족해 순회가 종료된다

for문을 통해 배열이나 문자열을 순회하는 것은 결국 매 루프마다 next 함수를 호출하는 것이라고 볼 수 있겠다

클로저니 렉시컬 스코프니 하는 용어들이 나왔는데, 함수형 프로그래밍 관련 용어니 이것도 다 정리해두는 게 좋겠다

 

let str = "";
for (let value of obj) str += value + " ";
console.log(str);

Symbol.iterator을 적용한 객체를 다시 반복문으로 순회해 보면 제대로 동작하여 str 문자열이 잘 만들어지는 것을 볼 수 있다 (신기방기)

Symbol.iterator은 이처럼 반복문으로 순회할 수 없는 객체를 순회가능하도록 만들어 준다

위의 예시는 단순히 객체의 값을 순서대로 순회하도록 했지만, 조건문이나 옵션을 추가해서 순회 방식을 커스텀할 수도 있다

 

let iterator = "hello world"[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

built-in iterator의 대표적인 예로 문자열 자료형 (String) 이 있는데, 실제로 Symbol.iterator 메서드를 불러와 next 함수를 호출할 때마다 내부적으로 index (또는 그러한 역할을 하는 변수) 의 값이 증가하기 때문에 value가 변하는 것을 볼 수 있다

배열이나 Map, Set 등의 자료형들도 내부적으론 Symbol.iterator을 갖고 있다

기타

이런 키워드나 자료형이 그렇듯이 어떻게 활용할지가 주 관건인 것 같다

Symbol.iterator은 객체를 배열처럼 순회할 수 있도록 다룰 때 종종 사용돼서 편리하더라

여러 코드를 리뷰하면 이런 새로운 개념들도 알게 되고 참 좋은 것 같다 (뜬금)


참고자료

Symbol.for() - JavaScript | MDN

Symbol.keyFor() - JavaScript | MDN

Symbol - JavaScript | MDN

Everything you need to know about JavaScript symbols

Iteration protocols - JavaScript | MDN

[JavaScript] 심볼 (Symbol) 타입 이해하기

Symbol | PoiemaWeb

Comments