ES6 In Depth: 심볼 (Symbol)

ES6 In Depth는 ECMAScript 표준의 6번째 에디션(줄여서 ES6)을 맞아 JavaScript에 새로 추가된 요소들을 살펴보는 시리즈입니다.

ES6 심볼(symbol)은 무엇일까요?

심볼은 로고가 아닙니다.

심볼은 당신이 코드에 쓸 수 있는 작은 그림이 아닙니다.

let ? = ? × ?;  // SyntaxError

심볼은 무엇인가를 기념하기 위해 만든 건물도 아닙니다.

심볼(Symbol)은 절대로 심벌즈(Cymbals)가 아닙니다.

(프로그래밍에 심벌즈를 쓰는 것은 좋은 생각이 아닙니다. 심벌즈는 파열음을 내기 때문입니다.)

그러니, 도대체 심볼이 뭘까요?

7번째 타입(type)

1997년 JavaScript가 처음 표준화된 이래로, JavaScript는 6개의 타입을 갖고 있었습니다. ES6가 발표되기 전까지, JS 프로그램 안의 모든 값은 반드시 다음 6개 타입들 중 하나였습니다.

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object

타입은 어떤 값들의 집합입니다. 처음 5개 타입은 모두 유한한 집합입니다. Boolean 타입의 값은 당연히 truefalse 2개 뿐입니다. 다른 값은 존재할 수 없습니다. Number 타입과 String 타입에는 조금 더 많은 값들이 존재합니다. 표준에 의하면 가능한 Number 타입 값은 18,437,736,874,454,810,627 개입니다 (NaN을 포함해서요. NaN은 “Not a Number”를 의미하는 값입니다). String 타입은 훨씬 더 많은 값을 가질 수 있습니다. 제 생각이 맞다면 (2144,115,188,075,855,872 − 1) ÷ 65,535 개입니다… 제가 잘못 계산했을 수도 있습니다.

그런데 Object 타입은 무한한 값이 존재할 수 있습니다. 모든 객체(object)는 고유하고 고귀한 눈꽃송이입니다. 당신이 웹 페이지를 열 때마다 엄청난 수의 새로운 객체들이 생성됩니다.

ES6 심볼은 어떤 값입니다. 하지만 심볼은 문자열이 아닙니다. 심볼은 객체가 아닙니다. 심볼은 뭔가 새로운 것입니다. 7번째 타입의 값입니다.

심볼이 유용한 시나리오에 대해 이야기해 봅시다.

간단한 불린(boolean) 값 한 개

가끔 어떤 정보를 다른 사람이 만든 JavaScript 객체에 덧붙일 수 있다는 사실이 무척 편리할 때가 있습니다.

예를 들어, 당신이 CSS 트랜지션을 이용해서 DOM 엘리먼트를 스크린 위에서 움직이게 하는 JS 라이브러리를 만드는 중이라고 가정해 봅시다. 당신은 div 엘리먼트 한개에 여러개의 CSS 트랜지션을 동시 적용할 경우 동작이 매끄럽지 못하다는 사실을 발견합니다. 그럴 경우 불연속적인 “점프” 현상이 발생했습니다. 당신은 이 문제를 해결할 수 있다고 생각합니다. 우선 주어진 엘리먼트가 이미 움직이고 있는 중인지 알아낼 방법이 필요합니다.

어떻게 이 문제를 해결할 수 있을까요?

한가지 방법은 CSS API를 이용해서 해당 엘리먼트가 움직이고 있는 중인지 브라우저에게 묻는 것입니다. 하지만 이 방법은 과한 것 같습니다. 당신의 라이브러리는 해당 엘리먼트가 움직이고 있는 중인지 이미 알고 있어야 합니다. 애당초 해당 엘리먼트가 움직이도록 설정하는 코드가 바로 당신 라이브러리이기 때문입니다!

당신이 정말 원하는 것은 어떤 엘리먼트가 움직이는 중인지 추적할 수 있는 방법입니다. 당신은 움직이고 있는 엘리먼트들의 배열을 만들 수도 있을 것입니다. 어떤 엘리먼트를 애니메이션시키기 위해 당신의 라이브러리가 호출될 때마다, 당신은 해당 배열을 탐색해서 그 엘리먼트가 이미 존재하는지 확인할 수 있습니다.

흠. 배열이 클 경우, 순차탐색(linear search)은 느릴 수 있습니다.

당신이 정말로 원하는 것은 해당 엘리먼트에 플랙(flag)을 설정하는 것 뿐입니다.

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

이 방법에도 잠재적인 문제가 있습니다. 당신의 코드가 DOM을 사용하는 유일한 코드가 아니기 때문입니다.

  1. for-inObject.keys()를 사용하는 기존 코드가 당신이 만든 속성 때문에 잘못 동작할 수 있습니다.
  2. 다른 영리한 라이브러리 작성자가 똑같은 기법을 더먼저 생각한 관계로, 당신의 라이브러리가 기존 라이브러리와 충돌할 수 있습니다.
  3. 다른 영리한 라이브러리 작성자가 똑같은 기법을 나중에 생각하는 바람에, 당신의 라이브러리가 미래의 라이브러리와 충돌할 수 있습니다.
  4. 표준화 위원회가 모든 엘리먼트에 .isMoving() 메소드를 추가하기로 결정할 수 있습니다. 그러면 당신은 정말로 망하는 겁니다!

물론 당신은 마지막 3개 문제를 회피하기 위해 어느 누구도 이름으로 사용하지 않을만한 아주 이상하고 아주 난해한 문자열을 쓸 수도 있습니다.

if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
  smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;

시각적 고통을 감수할만큼 가치 있는 방법은 아니라고 생각합니다.

속성 이름으로 쓰기 위해 암호 기술을 사용해서 실질적으로 고유함을 보장하는 문자열을 생성할 수도 있습니다.

// 1024 유니코드 난수 문자열 얻기
var isMoving = SecureRandom.generateName();

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

object[name] 문법은 글자 그대로 모든 문자열을 속성(property) name으로 사용할 수 있게 합니다. 그래서 이 방법은 통할 것입니다. 이름 중복은 실질적으로 불가능하니다. 그리고 이 코드는 보기에 괜찮습니다.

하지만 이 방법은 디버깅 하기가 아주 나쁩니다. 해당 속성을 갖는 엘리먼트를 console.log() 코드로 찍을 때마다, 당신은 아주 긴 난수 문자열을 보게 될 것입니다. 만약 이런 속성을 여러 개 써야한다면 어떻게 하나요? 그 속성들을 어떻게 관리할 셈인가요? 그 속성들은 당신이 페이지를 리로드(reload)할 때마다 다른 이름을 갖게 될 것입니다.

왜 이리 힘들죠? 우리는 단지 간단한 불린(boolean) 값 한 개를 원했을 뿐인데요!

심볼이 답입니다

심볼(Symbol)은 프로그램이 이름 충돌의 위험 없이 속성(property)의 키(key)로 쓰기 위해 생성하고 사용할 수 있는 값입니다.

var mySymbol = Symbol();

Symbol()을 호출하면 새로운 심볼 값이 생성됩니다. 이 값은 다른 어떤 값과도 다릅니다.

문자열 값이나 넘버 값처럼, 심볼 값도 속성(property)의 키(key)로 사용할 수 있습니다. 심볼 값은 다른 어떤 값과도 다르기 때문에, 심볼을 키로 갖는 속성은 다른 어떤 속성과도 충돌되지 않을 것을 보장 받습니다.

obj[mySymbol] = "ok!";  // 충돌되지 않음을 보장
console.log(obj[mySymbol]);  // ok!

앞서 논의한 상황에 심볼을 사용하면 다음과 같습니다.

// 고유한 심볼을 생성
var isMoving = Symbol("isMoving");

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

이 코드에 대한 추가 설명

  • Symbol("isMoving") 안에 있는 "isMoving" 문자열은 주석(description)입니다. 이 문자열은 디버깅할 때 유용합니다. 심볼 값을 console.log()로 찍거나, .toString()을 써서 심볼 값을 문자열로 바꾸거나, 에러 메시지에서 참조할 경우 이 문자열이 출력됩니다. 그게 전부입니다.
  • element[isMoving]심볼을 키로 갖는 속성(symbol-keyed property)입니다. 이것은 문자열이 아니라 심볼을 이름으로 갖는 속성일 뿐입니다. 그것만 제외하면 일반 속성과 모든 면에서 동일합니다.
  • 배열의 요소처럼, 심볼을 키로 갖는 속성(symbol-keyed property)은 obj.name 같이 점(dot)을 사용해서 접근할 수 없습니다. 심볼을 키로 갖는 속성은 반드시 꺽쇠 기호를 써서 접근해야 합니다.
  • 이미 심볼 값을 알고 있는 경우, 심볼을 키로 갖는 속성(symbol-keyed property)에 접근하는 것은 아주 쉬운 일입니다. 위의 예제가 element[isMoving] 속성에 접근해서 값을 읽거나 쓰는 방법을 보여주고 있습니다. 그리고 if (isMoving in element)처럼 속성을 참조하거나, delete element[isMoving]처럼 속성을 제거하는 것도 가능합니다.
  • 반면, 이런 모든 행위는 isMoving 심볼 값이 실행 스코프 안에 있을 때만 가능합니다. 이로인해 심볼을 간단한 캡슐화(encapsulation) 매카니즘으로 사용할 수 있습니다. 어떤 모듈이 스스로 심볼을 만드는 경우 해당 모듈은 해당 심볼을 모든 객체에 적용할 수 있습니다. 다른 코드가 만드는 속성과 충돌하는 것을 전혀 걱정할 필요가 없습니다.

심볼 키(key)는 충돌을 피하기 위해 만들어진 것이기 때문에, JavaScript의 일반적인 객체 조사(object-inspection) 기능들은 심볼 키(key)를 그냥 무시합니다. 예를 들어 for-in 루프의 경우, 객체의 문자열 키(key)들만 루프의 실행 대상으로 조회합니다. 심볼 키(key)는 건너뜁니다. Object.keys(obj)Object.getOwnPropertyNames(obj)도 마찬가지입니다. 하지만 심볼이 철저히 감춰지기만 하는 것은 아닙니다. 새로운 API Object.getOwnPropertySymbols(obj)를 사용하면 객체 안에 존재하는 심볼 키(key) 목록을 조회하는 것이 가능합니다. 또다른 새로운 API Reflect.ownKeys(obj)는 문자열 키(key)와 심볼 키(key)를 모두 리턴합니다. (Reflect API에 대해서는 다른 글을 통해 자세히 살펴보겠습니다.)

라이브러리들과 프레임워크들에게는 심볼의 다양한 용도가 유용할 것입니다. 그리고 나중에 살펴보겠지만 랭귀지 자체도 심볼을 다양한 용도로 사용하고 있습니다.

그런데 심볼이 정확히 뭔가요?

> typeof Symbol()
"symbol"

심볼은 다른 어떤 것과도 다른 것입니다.

심볼은 일단 생성되면 변경되지 않습니다. 심볼에는 속성을 부여할 수 없습니다 (만약 strict 모드에서 그런 시도를 하면 TypeError가 발생합니다). 심볼은 속성 이름으로 사용할 수 있습니다. 여기까지의 특성들은 모두 문자열과 유사합니다.

반면, 각 심볼은 고유합니다. 다른 심볼들과 구별됩니다 (같은 주석을 갖고 있더라도 다른 심볼입니다). 그리고 우리는 간단하게 새로운 심볼을 만들 수 있습니다. 여기까지의 특성들은 객체와 유사합니다.

ES6 심볼은 Lisp나 Ruby 같은 랭귀지의 좀 더 전통적인 심볼들과 비슷합니다. 하지만 랭귀지에 아주 밀접하게 통합되어 있지는 않습니다. Lisp 랭귀지의 경우 모든 식별자들은 심볼입니다. JS 랭귀지의 경우, 대부분의 식별자들과 속성 키(key)들은 여전히 문자열입니다. 심볼(Symbol)은 그냥 추가 옵션일 뿐입니다.

심볼에 관련된 주의사항이 있습니다. 랭귀지의 다른 요소들과 달리, 심볼은 문자열로 자동 변환되지 않습니다. 심볼을 다른 문자열에 이어붙이려고(concatenate)하면 TypeError가 발생합니다.

> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string

String(sym) 또는 sym.toString()을 써서 심볼을 명시적으로 문자열로 변환하면 TypeError를 피할 수 있습니다.

3가지 종류의 심볼

심볼을 얻는 3가지 방법이 있습니다.

  • Symbol()을 호출합니다. 이미 논의한 바와 같이, 이 메소드는 호출될 때마다 새롭고 고유한 심볼을 리턴합니다.
  • Symbol.for(string)을 호출합니다. 이 메소드는 심볼 레지스트리(symbol registry)라고 불리는 심볼들의 목록을 참조합니다. Symbol() 메소드가 만드는 고유한 심볼들과 달리, 심볼 레지스트리에 있는 심볼들은 공유됩니다. 만약 당신이 Symbol.for("cat")을 30번 호출한다면, 이 메소드는 매번 같은 심볼을 리턴할 것입니다. 심볼 레지스트리는 여러 개의 웹 페이지들, 또는 같은 페이지에 있는 여러 개의 모듈들이 심볼을 공유해야 하는 경우 유용합니다.
  • Symbol.iterator처럼 표준에 의해 정의된 심볼을 사용합니다. 표준이 자체적으로 정의하는 심볼이 몇 개 있습니다. 표준에 의해 정의된 심볼들은 나름의 특별한 용도가 있습니다.

만약 아직도 심볼의 유용성에 대한 확신이 서지 않는다면, 마지막 부류의 심볼들을 흥미있게 보시기 바랍니다. 왜냐하면 이들이 이미 현실 세계에서 심볼의 유용성을 증명하고 있기 때문입니다.

ES6 스펙이 심볼을 사용하는 방식

우리는 벌써 ES6가 기존 코드와의 충돌을 회피하기 위해 심볼을 사용하는 사례 한가지를 살펴봤습니다. 지난번 이터레이터(iterator)에 관한 글에서 우리는 for (var item of myArray) 루프가 myArray[Symbol.iterator]() 메소드를 호출하면서 시작하는 것을 보았습니다. 저는 이 메소드가 myArray.iterator()로 명명될 뻔했다고 설명했습니다. 하지만 심볼을 쓰는 것이 하위 호환성을 보장하기 때문에 더 나은 방법입니다.

이제 우리는 심볼이 무엇인지 알게 됐습니다. 심볼이 왜 만들어졌는지, 심볼이 무엇을 의미하는지 이해하는 것은 어렵지 않습니다.

여기 ES6가 스스로 정의한 심볼들을 이용하는 몇 가지 사례들이 있습니다. (이 기능들은 아직 Firefox에 구현되지 않았습니다.)

  • instanceof를 확장 가능하게 만듭니다. ES6에서, object instanceof constructor 구문은 생성자(constructor)의 메소드인 constructor[Symbol.hasInstance](object)로 규정됩니다. 이는 이 구문이 확장 가능하다는 의미입니다.
  • 새로운 기능과 예전 코드의 충돌을 제거합니다. 잘 알려져있지 않지만, 우리는 어떤 ES6 Array 메소드들이 단지 정의되어 있다는 이유로 기존 웹사이트들의 동작에 오류를 일으킨다는 사실을 발견했습니다. 다른 웹 표준들에도 비슷한 문제가 있었습니다. 단지 브라우저에 새로운 메소드를 추가했을 뿐인데 기존 사이트들의 동작이 방해 받는 것입니다. 동작 오류의 주요 원인은 동적 스코핑(dynamic scoping) 기능 때문이었습니다. 그래서 ES6는 특별한 심볼을 도입했습니다. Symbol.unscopables입니다. 웹 표준은 Symbol.unscopables을 이용해서 특정 메소드들이 동적 스코핑에 관여되는 것을 방지할 수 있습니다.
  • 새로운 종류의 문자열 검색(string-matching)을 지원합니다. ES5에서 str.match(myObject) 코드는 myObjectRegExp로 변환시키려고 시도합니다. ES6에서 이 코드는 우선 myObjectmyObject[Symbol.match](str) 메소드를 갖고 있는지 확인합니다. 이제 라이브러리들은 RegExp 객체가 사용되는 모든 곳에 커스텀 문자열 파싱(string-parsing) 클래스를 제공할 수 있습니다.

이 이용 사례들은 무척 제한적입니다. 이 이용 사례들 중 어떤 것도 제가 매일 쓰는 코드에 큰 영향을 주고 있지 않습니다. 표준이 정의한 심볼들은 장기적인 관점에서 유용합니다. 표준이 정의한 심볼들은 PHP와 Python에 있는 __doubleUnderscores을 JavaScript 방식으로 개선시킨 버전입니다. 장차 표준은 당신의 기존 코드에 아무런 위협을 주지 않고 랭귀지에 새로운 hook을 추가하기 위해 이들을 이용할 것입니다.

언제 ES6 심볼을 사용할 수 있나요?

심볼은 Firefox 36과 Chrome 38에 구현되어 있습니다. Firefox에 심볼을 구현하는 일은 제가 직접 했습니다. 그러니 만약 심볼이 심벌즈처럼 동작한다면, 당신은 누구에게 불평해야 하는지 알 것입니다.

아직 ES6 심볼을 지원하지 브라우저들의 경우, core.js 같은 폴리필(polyfill)을 사용할 수 있습니다. 심볼이 지금까지 랭귀지에 존재했던 것들과 전혀 다른 것이기 때문에 폴리필이 완벽하지 않습니다. 주의사항을 읽어보세요.

다음에 우리는 새로운 글 2개를 준비하려고 합니다. 우선, 오랫동안 기다린 끝에 마침내 ES6를 맞아 JavaScript에 안착한 기능들을 알아보고 불평도 해볼 것입니다. 우리는 프로그래밍의 거의 초창기 시절로 거슬러 올라가는 2개의 기능들에 대해 이야기를 시작할 것입니다. 그리고 환경의 변화 때문에 중요해진 2개의 기능들에 대해 이야기를 계속할 것입니다. 그러니 ES6 컬렉션(collection)에 대해 자세히 살펴보는 다음 글을 함께 해주세요.

그리고, Gastón Silva가 쓸 다른 글도 함께 해주세요. 이 글은 ES6의 기능에 대한 글이 전혀 아닙니다. 하지만 당신이 프로젝트에 ES6를 쓰기 시작할 때 반드시 필요한 내용입니다. 다음에 봅시다!

이 글은 가 쓴 ES6 In Depth: Symbols의 한국어 번역본입니다.

작성자: ingeeKim

"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.

ingeeKim가 작성한 문서들…


5 댓글

  1. 김민재

    감사합니다.

    1월 3rd, 2019 at 1:17 오후
  2. ingeeKim

    읽어주셔서 감사해요. ^^

    1월 3rd, 2019 at 1:29 오후
  3. 구건모

    덕분에 잘 읽고 갑니다.

    6월 16th, 2019 at 10:32 오후
  4. Peter

    이해가 쉽고 글이 재밌어서 술술 읽히내요 감사히 읽었습니다.

    4월 3rd, 2020 at 3:58 오후
  5. ingeeKim

    저도 번역하면서 즐거웠습니다. 저자가 존경스러워요.

    4월 10th, 2020 at 10:58 오전

댓글 쓰기