구리

[JavaScript] Symbol을 사용하는 이유 본문

JavaScript

[JavaScript] Symbol을 사용하는 이유

guriguriguri 2023. 7. 20. 23:18

자바스크립트의 데이터 타입에는 크게 2가지로 기본형(원시형), 참조형이 있습니다. 기본형에는 number, string, boolean, null, undefined 가 있으며 ES2015(ES6)에 추가된 Symbol이 있습니다. Symbol은 무엇이며 언제 사용하고 왜 추가된 걸까요?

Symbol이란

자바스크립트의 객체 타입은 순서가 없는 프로퍼티 집합으로 ES6 이전까지는 프로퍼티명이 오로지 문자열이었지만 ES6 이후에는 심벌 타입도 프로퍼티명으로 지정할 수 있게 되었습니다.

const strname = "string name";
const symname = Symbol("propname");
console.log(typeof strname); // string
console.log(typeof symname); // symbol
const o = {};
o[strname] = 1;
o[symname] = 2;
console.log(o[strname]);  // 1
console.log(o[symname]);  // 2
자바스크립트의 객체의 프로퍼티는 정말 순서가 없을까요?
const ID = Symbol("id");
const person = {
  [ID]: "Symbol",
  1: "one",
  name: "john",
  2: "two",
};

console.log(person); // { '1': 'one', '2': 'two', name: 'john', [Symbol(id)]: 'Symbol' }​

객체는 순서가 없는 프로퍼티 모음이라 순서가 보장되지 않지만 어떤 규칙이 있는 것 같아서 찾아봤습니다. (관련 링크)
ES2015 기준으로 프로퍼티 순서의 규칙은 아래와 같이 정의됩니다.
  1. 오름차순의 정수
  2. 삽입 순서순의 문자열
  3. 삽입 순서순의 Symbol
위 순서는 ES2015 기준으로, 일부 구형 브라우저는 이를 보장하지 않았지만 ES2015 기준 아래의 메서드들이 이 순서를 보장해줬습니다.
Object.assign
Object.defineProperties
Object.getOwnPropertyNames
Object.getOwnPropertySymbols
Reflect.ownKeys

그리고 ES2020 기준 아래 메서드들을 포함한 메서드들이 순서를 보장합니다.

Object.key
Object.entries
Object.values
for..in

모든 브라우저에서 ES2020을 지원해주지 않으며, ES2015에서도 보장이 안되는 상황이 존재하기에 순서가 보장되어야 한다면 삽입 순서를 보장해주는 Map, 삽입 순서만 필요하다면 배열이나 Set을 사용하는 것이 좋을 것 같습니다.

 

심벌 타입에는 리터럴 문법이 없으며 심벌 값을 가져올 때 Symbol()함수를 호출합니다. 또한 같은 인자로 호출해도 심벌은 고유하고 불변한 값이기에 다른 값을 반환합니다. (Symbol.for() 예외)

따라서 심벌값을 프로퍼티명으로 사용시, 심벌을 공유하지 않는 이상 프로그램의 다른 모듈에서 실수로 프로퍼티를 덮어쓸 일은 없습니다.

ES6에서 for/of 루프, 이터러블 객체 도입시, 클래스가 자기 자신을 이터러블로 만들 수 있는 표준 메서드를 정의해야 했습니다. 그런데 특정 문자열 이름을 이터레이터 메서드로 표준화시, 기존 코드가 깨지게 되어 심벌 이름을 도입하였습니다.

Symbol.iterator는 객체를 이터러블로 만드는 메서드 이름으로 쓸 수 있는 심벌 값입니다.

즉 심벌의 목적은 아래처럼 요약할 수 있습니다.

  • 심벌은 다른 어떤 심벌과도 같지 않기에 고유한 프로퍼티명을 만들 때 좋음
  • 보안이 목적이 아니며, 자바스크립트 객체가 사용할 수 있는 안전한 확장 매커니즘을 정의한 것
    • 안전한 확장 - 제어 불가한 서드파티 코드에서 가져온 객체에 프로퍼티 추가시, 프로퍼티명이 겹칠 수도 있는 상황이라면 심벌을 사용
    • 보안이 목적이 아님 - 서드 파티 코드에서 Object.getOwnPropertySymbols()로 접근시 심벌 확인 및 수정, 삭제 가능
리터럴
사람이 이해할 수 있는 문자 혹은 약속된 기호를 사용해 값을 생성하는 표기법으로 자바스크립트 엔진은 코드가 실행되는 런타임 시점에 리터럴을 평가해 값을 생성합니다.

 

 

심벌 생성시 문자열 인수를 전달할 수 있는데 심벌값에 대한 설명을 의미하며 디버깅용으로만 사용됩니다.

const uniqueSymbol = Symbol("unique");
console.log(uniqueSymbol.description); // unique
console.log(uniqueSymbol.toString()); // Symbol(unique)

 

심벌값은 암묵적으로 문자열이나 숫자 타입으로 변환하지 않습니다. 하지만 불리언 타입으로는 암묵적으로 타입 변환이 이뤄집니다.

const uniqueSymbol = Symbol();
console.log(uniqueSymbol + ""); // Cannot convert a Symbol value to a string
console.log(+uniqueSymbol); // Cannot convert a Symbol value to a number
console.log(!!uniqueSymbol); // true
if (uniqueSymbol) console.log("uniqueSymbol is not empty"); // true

또한 심벌 값도 문자열, 숫자, 불리언과 같이 객체처럼 접근시 암묵적으로 래퍼 객체를 생성합니다. description 프로퍼티, toString 메서드는 Symbol.prototype의 프로퍼티입니다.

래퍼 객체
const str = "string data";
console.log(str.length); // 1
console.log(str.toUpperCase()); // STRING DATA​

위는 원시 타입인 문자열이 프로퍼티와 메서드를 갖고 있는 객체처럼 동작합니다.

원시값의 경우, 객체처럼 마침표 표기법(혹은 대괄호 표기법)으로 접근시 JS 엔진이 일시적으로 원시값을 연관된 객체로 변환합니다.
그리고 변환된 객체로 프로퍼티 접근 및 메서드 호출 후 원시값으로 되돌립니다.

위 과정을 자세히 설명하면 다음과 같습니다.
   1. 위 예시를 기준으로, String 빌트인 생성자 함수를 이용해 인스턴스를 생성합니다.
   2. String 생성자 함수를 이용해 생성된 인스턴스의 내부 슬롯인 [[StringData]]에 원시값이 할당되고 해당 인스턴스는       String.prototype의 메서드를 상속받아 사용합니다.
   3. 래퍼 객체 처리 종료시, 식별자를 [[StringData]] 내부 슬롯에 할당된 원시값으로 되돌리고 래퍼 객체는 가비지 컬렉션의 대상이 됩니다.

즉, 래퍼 객체란 문자열, 숫자, 불리언 값에 대해 객체처럼 접근시 생성되는 임시 객체를 의미하며 이때 래퍼 객체를 생성하기 위해 String, Number, Boolean과 같은 빌트인 생성자 함수가 필요합니다.

 

전역 심볼 레지스트리

다른 코드에서도 쓸 수 있도록 심벌 정의 및 공유하기 위해 Symbol.for(), Symbol.keyFor()를 이용해 전역 심벌 레지스트리 정의할 수 있습니다. (Symbol.iterator 매커니즘과 같은 일종의 확장)

해당 메서드들을 이용해 전역 심벌 레지스트리 테이블과 런타임 환경 사이에서 심벌 값을 전해주는 역할을 합니다. 전역 심벌 레지스트리는 대부분 자바스크립트 컴파일 인프라에 내장되어 있고, 레지스트리 내용은 자바스크립트 런타임 환경에서 위 메서드를 사용하지 않는 이상 접근이 불가능합니다.

const a = Symbol.for("shared");
const b = Symbol.for("shared");
console.log(a === b); // true
console.log(a.toString());  // Symbol(shared)
console.log(Symbol.keyFor(b)); // shared

 

Symbol Use Case

  • 프로퍼티 은닉
    • 내 코드에서만 쓸 수 있도록 비공개로 둬 다른 코드의 프로퍼티와 충돌 방지
    • for..in이나 Object.keys, Object.getOwnPropertyNames 메서드로 심벌 키를 찾을 수 없기에 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있음
    • 단, ES2015에 도입된 Object.getOwnPropertySymbols 메서드 사용시 심볼 값을 프로퍼티 키로 사용해 생성한 프로퍼티를 찾을 수 있음
  • 값 변경 및 중복 방지 (JS에서 enum 사용하기)
    • 상수 중에 값에는 특별한 의미가 없고 상수 이름 자체에 의미가 있는 경우, 상수값 변경 및 다른 변수값과 중복 방지를 위해 유일뮤이한 심벌값 사용 (아래 예시에서는 자바스크립트에서 enum을 따라하기 위해 객체 동결을 위한 Object.freeze와 심벌을 사용)
// case
const Direction = {
  UP: 1,
  DOWN: 2,
  LEFT: 3,
  RIGHT: 4,
};

// better case
const Direction = Object.freeze({
	UP: Symbol('up'),
	DOWN: Symbol('down'),
	LEFT: Symbol('left'),
	RIGHT: Symbol('right'),
})
  • 심볼과 표준 빌트인 객체 확장
    • 빌트인 객체에 사용자 정의 메서드 추가시, ECMAScript에 중복 메서드명이 존재하면 문제가 될 수 있기에, 중복 가능성이 없는 심벌 값으로 빌트인 객체 확장시 충돌 위험이 없어져 안전한 빌트인 객체 확장 가능
String.prototype[Symbol.for("caseInsensitiveSearch")] = function (target) {
  return this.toLowerCase().indexOf(target);
};

console.log("stringData"[Symbol.for("caseInsensitiveSearch")]("data")); // 6

 

빌트인 객체
자바스크립트 객체는 크게 3가지로 분류할 수 있습니다.

1. 표준 빌트인 객체
ECMAScript 명세에 정의된 객체로, 애플리케이션 전역의 공통 기능을 제공하며 실행 환경(브라우저 혹은 Node.js)에 상관 없이 언제나 사용할 수 있습니다. (String, Number, Object 등)

2. 호스트 객체
JS 실행환경에서 추가로 제공하는 객체로, 브라우저 환경에서는 window, DOM 등이 있으며 Node.js에서는 Node.js의 고유한 API를 호스트 객체로 제공합니다.

3. 사용자 정의 객체
사용자가 직접 정의한 객체를 뜻합니다.

Well-known Symbol

Symbol 함수의 프로퍼티에 할당된 빌트인 심벌값을 ECMAScript 사양에서는 Well-Known Symbol이라고 하며 JS 엔진의 내부 알고리즘에 사용됩니다.

Array, String, Map 등과 같이 for...of문으로 순회 가능한 빌트인 이터러블은 Well-Known Symbol인 Symbol.iterator를 키로 갖는 메서드를 가지며 이때 해당 메서드 호출시 이터레이터를 반환합니다. 즉, 빌트인 이터러블은 이터레이션 프로토콜을 준수합니다.

일반 객체를 이터러블처럼 동작하도록 구현할 경우, 이터레이션 프로토콜을 따르기 위해 Symbol.iterator를 키로 갖는 메서드를 객체에 추가하고 이터레이터를 반환하도록 구현하면 됩니다. 이때, 일반 객체에 추가해야 하는 메서드의 키인 Symbol.iterator는 기존 프로퍼티 키 또는 미래에 추가될 프로퍼티 키와 절대로 중복되지 않기에 하위 호환성을 보장하기 위해 도입된 것입니다.

const iterable = {
  [Symbol.iterator]() {
    let cur = 1;
    const max = 5;
    return {
      next() {
        return { value: cur++, done: cur > max + 1 };
      },
    };
  },
};

for (const num of iterable) {
  console.log(num); // 1 2 3 4 5 
}

 

 

추가할 것

  • 래퍼 객체가 탄생한 이유
  • well known symbol 추가

 

참고 자료

 

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol

http://hacks.mozilla.or.kr/2015/09/es6-in-depth-symbols/

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects

https://product.kyobobook.co.kr/detail/S000001033131

https://product.kyobobook.co.kr/detail/S000001766445