포스트

JavaScript Symbol 완벽 가이드 - 고유 식별자와 메타프로그래밍

JavaScript Symbol의 개념부터 Well-known Symbols, Symbol.for() 활용까지. 고유 식별자 생성, 프로퍼티 충돌 방지, 메타프로그래밍 핵심을 마스터합니다.

JavaScript Symbol 완벽 가이드 - 고유 식별자와 메타프로그래밍

개요

JavaScript에서 객체의 프로퍼티 키는 전통적으로 문자열만 사용할 수 있었습니다. 하지만 라이브러리 간 프로퍼티 이름 충돌, 진정한 프라이빗 프로퍼티의 부재, 내장 객체 동작 커스터마이징의 어려움 등 여러 한계가 있었습니다.

ES6에서 도입된 Symbol은 이러한 문제를 해결하기 위한 7번째 원시 타입입니다. Symbol은 생성될 때마다 고유하고 변경 불가능한 값을 만들어내며, 객체 프로퍼티의 키로 사용할 수 있습니다. 특히 Well-known Symbols을 통해 JavaScript 엔진의 내부 동작을 커스터마이징하는 메타프로그래밍이 가능해졌습니다.

학습 목표

  • Symbol의 기본 개념과 특성(고유성, 불변성) 이해
  • Symbol.for()와 Symbol()의 차이점 파악
  • Well-known Symbols을 활용한 메타프로그래밍 습득
  • 프라이빗 프로퍼티, 프로퍼티 충돌 방지 등 실전 활용법 마스터
  • Symbol vs WeakMap 비교를 통한 적절한 사용 시나리오 파악

사전 지식


Symbol 기본 개념

7번째 원시 타입

JavaScript에는 7가지 원시 타입이 있습니다: undefined, null, boolean, number, string, bigint, 그리고 ES6에서 추가된 symbol입니다.

1
2
3
4
5
6
7
8
9
10
11
// Symbol 생성
const sym1 = Symbol();
const sym2 = Symbol();

console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false - 모든 Symbol은 고유함

// 설명(description) 추가 가능
const sym3 = Symbol('mySymbol');
console.log(sym3.description); // "mySymbol" (ES2019+)
console.log(sym3.toString()); // "Symbol(mySymbol)"

Symbol의 가장 중요한 특성은 고유성(uniqueness)입니다. 같은 설명을 가진 Symbol이라도 서로 다릅니다.

1
2
3
4
5
const sym1 = Symbol('id');
const sym2 = Symbol('id');

console.log(sym1 === sym2); // false
console.log(sym1 == sym2); // false (동등 비교도 false)

Symbol 생성 방법

Symbol() - 로컬 Symbol 생성

1
2
3
4
5
6
7
8
9
// 기본 생성
const localSymbol = Symbol();

// 설명과 함께 생성 (디버깅 용도)
const userId = Symbol('user id');
const secretKey = Symbol('secret key');

// Symbol은 new 키워드로 생성할 수 없음
// const sym = new Symbol(); // TypeError!

Symbol.for() - 전역 Symbol 레지스트리

Symbol.for()는 전역 Symbol 레지스트리에서 Symbol을 검색하거나 생성합니다. 같은 키로 호출하면 동일한 Symbol을 반환합니다.

1
2
3
4
5
6
7
8
9
// 전역 레지스트리에 Symbol 생성
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');

console.log(globalSym1 === globalSym2); // true - 같은 Symbol!

// 로컬 Symbol과의 차이
const localSym = Symbol('app.id');
console.log(globalSym1 === localSym); // false

Symbol.keyFor() - 전역 Symbol의 키 조회

1
2
3
4
5
const globalSym = Symbol.for('myKey');
const localSym = Symbol('myKey');

console.log(Symbol.keyFor(globalSym)); // "myKey"
console.log(Symbol.keyFor(localSym)); // undefined (로컬 Symbol)

전역 Symbol은 모듈 간, 심지어 다른 realm(iframe 등) 간에도 공유됩니다.

1
2
3
4
5
6
7
// 모듈 A
export const PLUGIN_ID = Symbol.for('myLibrary.pluginId');

// 모듈 B (같은 Symbol 참조)
import { PLUGIN_ID } from './moduleA';
const sameSymbol = Symbol.for('myLibrary.pluginId');
console.log(PLUGIN_ID === sameSymbol); // true

Symbol의 특징

타입 변환

Symbol은 다른 원시 타입과 달리 암시적 타입 변환이 제한됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sym = Symbol('test');

// 문자열 변환
console.log(String(sym)); // "Symbol(test)"
console.log(sym.toString()); // "Symbol(test)"

// 암시적 문자열 변환은 에러 발생
// console.log('Symbol: ' + sym); // TypeError!
// console.log(`Symbol: ${sym}`); // TypeError!

// 숫자 변환 불가
// console.log(+sym); // TypeError!
// console.log(sym * 2); // TypeError!

// 불리언 변환은 가능
console.log(Boolean(sym)); // true
console.log(!sym); // false

if (sym) {
  console.log('Symbol은 truthy'); // 출력됨
}

열거 불가능 (Non-enumerable)

Symbol 키 프로퍼티는 일반적인 열거 메서드에서 제외됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const id = Symbol('id');
const secret = Symbol('secret');

const user = {
  name: '김철수',
  age: 30,
  [id]: 'user_123',
  [secret]: 'password123'
};

// for...in에서 Symbol 키는 제외
for (const key in user) {
  console.log(key); // "name", "age" 만 출력
}

// Object.keys()에서도 제외
console.log(Object.keys(user)); // ["name", "age"]

// Object.values(), Object.entries()도 마찬가지
console.log(Object.entries(user)); // [["name", "김철수"], ["age", 30]]

// Symbol 프로퍼티 조회 방법
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(secret)]

// Reflect.ownKeys()는 모든 키(문자열 + Symbol) 반환
console.log(Reflect.ownKeys(user)); // ["name", "age", Symbol(id), Symbol(secret)]

JSON 직렬화

Symbol 키 프로퍼티는 JSON 직렬화에서 완전히 무시됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const metadata = Symbol('metadata');

const data = {
  title: '블로그 포스트',
  content: 'Symbol에 대한 내용...',
  [metadata]: { createdAt: new Date(), version: 1 }
};

console.log(JSON.stringify(data));
// {"title":"블로그 포스트","content":"Symbol에 대한 내용..."}
// Symbol 키 프로퍼티는 포함되지 않음

// 직렬화가 필요한 경우 별도 처리 필요
function customStringify(obj) {
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  const combined = { ...obj };

  symbolKeys.forEach(sym => {
    combined[`__symbol_${sym.description}`] = obj[sym];
  });

  return JSON.stringify(combined);
}

Well-known Symbols

Well-known Symbols는 JavaScript 엔진의 내부 동작을 커스터마이징할 수 있는 특수한 Symbol들입니다. 이들을 통해 메타프로그래밍이 가능해집니다.

Symbol.iterator - 이터레이터 프로토콜

Symbol.iterator는 객체를 이터러블(반복 가능)하게 만드는 메서드를 정의합니다. 이터레이터에 대한 자세한 내용은 JavaScript 제너레이터와 이터레이터 가이드를 참고하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 커스텀 이터러블 객체
const range = {
  start: 1,
  end: 5,

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// for...of 사용 가능
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// 스프레드 연산자 사용 가능
console.log([...range]); // [1, 2, 3, 4, 5]

// Array.from() 사용 가능
console.log(Array.from(range)); // [1, 2, 3, 4, 5]

제너레이터를 활용한 더 간결한 구현:

1
2
3
4
5
6
7
8
9
10
11
12
const range = {
  start: 1,
  end: 5,

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
};

console.log([...range]); // [1, 2, 3, 4, 5]

Symbol.toStringTag - 객체 타입 태그

Symbol.toStringTagObject.prototype.toString()의 결과를 커스터마이징합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomCollection {
  get [Symbol.toStringTag]() {
    return 'CustomCollection';
  }
}

const collection = new CustomCollection();
console.log(Object.prototype.toString.call(collection));
// "[object CustomCollection]"

// 내장 객체들도 이를 사용
console.log(Object.prototype.toString.call(new Map()));
// "[object Map]"
console.log(Object.prototype.toString.call(new Set()));
// "[object Set]"
console.log(Object.prototype.toString.call(Promise.resolve()));
// "[object Promise]"

실용적인 활용 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Logger {
  constructor(name) {
    this.name = name;
  }

  get [Symbol.toStringTag]() {
    return `Logger:${this.name}`;
  }
}

const appLogger = new Logger('App');
console.log(`${Object.prototype.toString.call(appLogger)}`);
// "[object Logger:App]"

// 타입 체크 유틸리티
function getType(value) {
  return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(getType(appLogger)); // "Logger:App"
console.log(getType(new Map())); // "Map"
console.log(getType([])); // "Array"

Symbol.hasInstance - instanceof 커스터마이징

Symbol.hasInstanceinstanceof 연산자의 동작을 커스터마이징합니다.

1
2
3
4
5
6
7
8
9
10
class MyArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MyArray); // true
console.log([1, 2, 3] instanceof MyArray); // true
console.log({} instanceof MyArray); // false
console.log('string' instanceof MyArray); // false

덕 타이핑(Duck Typing) 구현:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Iterable {
  static [Symbol.hasInstance](instance) {
    return instance != null &&
           typeof instance[Symbol.iterator] === 'function';
  }
}

console.log([1, 2, 3] instanceof Iterable); // true
console.log('hello' instanceof Iterable); // true
console.log(new Map() instanceof Iterable); // true
console.log({ a: 1 } instanceof Iterable); // false

// 실용적 활용
function processIterable(data) {
  if (!(data instanceof Iterable)) {
    throw new TypeError('Iterable expected');
  }
  return [...data];
}

Symbol.toPrimitive - 타입 변환 제어

Symbol.toPrimitive는 객체가 원시 값으로 변환될 때의 동작을 정의합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const money = {
  amount: 1000,
  currency: 'KRW',

  [Symbol.toPrimitive](hint) {
    console.log(`Hint: ${hint}`);

    switch (hint) {
      case 'number':
        return this.amount;
      case 'string':
        return `${this.amount} ${this.currency}`;
      case 'default':
        return this.amount;
    }
  }
};

// 숫자 컨텍스트 (hint: "number")
console.log(+money); // 1000
console.log(money * 2); // 2000

// 문자열 컨텍스트 (hint: "string")
console.log(String(money)); // "1000 KRW"
console.log(`${money}`); // "1000 KRW"

// 기본 컨텍스트 (hint: "default")
console.log(money + 500); // 1500
console.log(money == 1000); // true

날짜 객체 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CustomDate {
  constructor(date) {
    this.date = new Date(date);
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return this.date.getTime();
      case 'string':
        return this.date.toLocaleDateString('ko-KR');
      default:
        return this.date.toISOString();
    }
  }
}

const today = new CustomDate('2025-12-20');

console.log(+today); // 1734652800000 (타임스탬프)
console.log(String(today)); // "2025. 12. 20."
console.log(today + ''); // "2025-12-20T00:00:00.000Z"

Symbol.species - 파생 객체 생성자 지정

Symbol.species는 파생 객체를 생성할 때 사용할 생성자를 지정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyArray extends Array {
  // map, filter 등이 MyArray가 아닌 Array를 반환하도록 설정
  static get [Symbol.species]() {
    return Array;
  }
}

const myArr = new MyArray(1, 2, 3);
console.log(myArr instanceof MyArray); // true

const mapped = myArr.map(x => x * 2);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true

Promise에서의 활용:

1
2
3
4
5
6
7
8
9
10
11
12
class TrackedPromise extends Promise {
  static get [Symbol.species]() {
    return Promise; // then()이 일반 Promise 반환
  }
}

const tracked = new TrackedPromise(resolve => resolve(1));
const chained = tracked.then(x => x + 1);

console.log(tracked instanceof TrackedPromise); // true
console.log(chained instanceof TrackedPromise); // false
console.log(chained instanceof Promise); // true

기타 Well-known Symbols

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Symbol.isConcatSpreadable - 배열 병합 동작 제어
const arr = [1, 2, 3];
arr[Symbol.isConcatSpreadable] = false;
console.log([0].concat(arr)); // [0, [1, 2, 3]]

// Symbol.match, Symbol.replace, Symbol.search, Symbol.split
// 문자열 메서드의 동작 커스터마이징
class CaseInsensitiveMatcher {
  constructor(pattern) {
    this.pattern = pattern.toLowerCase();
  }

  [Symbol.match](string) {
    const index = string.toLowerCase().indexOf(this.pattern);
    if (index === -1) return null;
    return [string.slice(index, index + this.pattern.length)];
  }
}

console.log('Hello World'.match(new CaseInsensitiveMatcher('WORLD')));
// ["World"]

// Symbol.unscopables - with 문에서 제외할 프로퍼티 지정 (거의 사용 안 함)

실전 활용 사례

프라이빗 프로퍼티 구현

Symbol을 사용하여 “준 프라이빗” 프로퍼티를 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 모듈 스코프에서 Symbol 정의
const _balance = Symbol('balance');
const _transactions = Symbol('transactions');

class BankAccount {
  constructor(initialBalance) {
    this[_balance] = initialBalance;
    this[_transactions] = [];
  }

  deposit(amount) {
    this[_balance] += amount;
    this[_transactions].push({ type: 'deposit', amount, date: new Date() });
  }

  withdraw(amount) {
    if (amount > this[_balance]) {
      throw new Error('잔액 부족');
    }
    this[_balance] -= amount;
    this[_transactions].push({ type: 'withdraw', amount, date: new Date() });
  }

  getBalance() {
    return this[_balance];
  }

  getTransactionHistory() {
    return [...this[_transactions]]; // 복사본 반환
  }
}

const account = new BankAccount(1000);
account.deposit(500);

console.log(account.getBalance()); // 1500
console.log(Object.keys(account)); // [] - Symbol 프로퍼티는 숨겨짐

// 하지만 완전히 프라이빗하지 않음
console.log(Object.getOwnPropertySymbols(account)); // [Symbol(balance), Symbol(transactions)]

객체 프로퍼티 충돌 방지

라이브러리에서 객체에 메타데이터를 추가할 때 Symbol을 사용하면 사용자 프로퍼티와 충돌을 방지할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 라이브러리 A
const LIBRARY_A_DATA = Symbol('libraryA.data');

function addLibraryAFeature(obj) {
  obj[LIBRARY_A_DATA] = {
    version: '1.0',
    initialized: true
  };
}

// 라이브러리 B (같은 설명을 사용해도 충돌 없음)
const LIBRARY_B_DATA = Symbol('libraryB.data');

function addLibraryBFeature(obj) {
  obj[LIBRARY_B_DATA] = {
    config: {},
    active: true
  };
}

// 사용자 코드
const myObject = {
  name: '내 객체',
  data: '사용자 데이터' // 문자열 'data' 키
};

addLibraryAFeature(myObject);
addLibraryBFeature(myObject);

// 모든 데이터가 충돌 없이 공존
console.log(myObject.data); // "사용자 데이터"
console.log(myObject[LIBRARY_A_DATA]); // { version: '1.0', initialized: true }
console.log(myObject[LIBRARY_B_DATA]); // { config: {}, active: true }

커스텀 이터레이터 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 페이지네이션 이터레이터
class Paginator {
  constructor(items, pageSize = 10) {
    this.items = items;
    this.pageSize = pageSize;
  }

  *[Symbol.iterator]() {
    for (let i = 0; i < this.items.length; i += this.pageSize) {
      yield this.items.slice(i, i + this.pageSize);
    }
  }

  // 역방향 이터레이션
  *reversed() {
    for (let i = this.items.length; i > 0; i -= this.pageSize) {
      const start = Math.max(0, i - this.pageSize);
      yield this.items.slice(start, i);
    }
  }
}

const items = Array.from({ length: 25 }, (_, i) => `Item ${i + 1}`);
const paginator = new Paginator(items, 10);

for (const page of paginator) {
  console.log(`페이지: ${page.length}개 항목`);
}
// 페이지: 10개 항목
// 페이지: 10개 항목
// 페이지: 5개 항목

// 이중 연결 리스트 이터레이터
class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
  }

  append(value) {
    const node = { value, prev: this.tail, next: null };
    if (this.tail) {
      this.tail.next = node;
    } else {
      this.head = node;
    }
    this.tail = node;
    return this;
  }

  *[Symbol.iterator]() {
    let current = this.head;
    while (current) {
      yield current.value;
      current = current.next;
    }
  }

  *reverse() {
    let current = this.tail;
    while (current) {
      yield current.value;
      current = current.prev;
    }
  }
}

const list = new DoublyLinkedList();
list.append(1).append(2).append(3);

console.log([...list]); // [1, 2, 3]
console.log([...list.reverse()]); // [3, 2, 1]

프레임워크/라이브러리에서의 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// React 스타일 컴포넌트 타입 식별
const REACT_ELEMENT_TYPE = Symbol.for('react.element');
const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');

function createElement(type, props, ...children) {
  return {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    props: { ...props, children },
    key: props?.key || null
  };
}

function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

// Redux 액션 타입
const ActionTypes = {
  ADD_TODO: Symbol('ADD_TODO'),
  REMOVE_TODO: Symbol('REMOVE_TODO'),
  TOGGLE_TODO: Symbol('TOGGLE_TODO')
};

// Symbol을 키로 사용하면 실수로 문자열과 충돌하지 않음
function todoReducer(state = [], action) {
  switch (action.type) {
    case ActionTypes.ADD_TODO:
      return [...state, action.payload];
    case ActionTypes.REMOVE_TODO:
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

Symbol vs WeakMap - 프라이빗 데이터 비교

프라이빗 데이터를 구현하는 두 가지 주요 방법을 비교해봅시다. Map과 WeakMap에 대한 자세한 내용은 JavaScript Map과 Set 완벽 가이드를 참고하세요.

Symbol 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _name = Symbol('name');
const _age = Symbol('age');

class Person {
  constructor(name, age) {
    this[_name] = name;
    this[_age] = age;
  }

  greet() {
    return `안녕하세요, ${this[_name]}입니다.`;
  }
}

const person = new Person('김철수', 30);

WeakMap 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const privateData = new WeakMap();

class Person {
  constructor(name, age) {
    privateData.set(this, { name, age });
  }

  greet() {
    const { name } = privateData.get(this);
    return `안녕하세요, ${name}입니다.`;
  }
}

const person = new Person('김철수', 30);

비교 분석

특성SymbolWeakMap
프라이빗 수준준 프라이빗 (발견 가능)진정한 프라이빗
접근 방법Object.getOwnPropertySymbols()불가능
성능직접 접근 (빠름)해시 조회 (약간 느림)
메모리인스턴스에 저장별도 WeakMap에 저장
상속가능복잡함
디버깅쉬움 (프로퍼티 보임)어려움
JSON 직렬화자동 제외별도 처리 필요

현대적 대안: Private Class Fields

ES2022부터 # 접두사를 사용한 진정한 프라이빗 필드가 지원됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
  #name;
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  greet() {
    return `안녕하세요, ${this.#name}입니다.`;
  }
}

const person = new Person('김철수', 30);
// person.#name; // SyntaxError: Private field

선택 가이드:

  • Symbol: 열거에서 숨기고 싶지만 리플렉션 접근은 허용할 때
  • WeakMap: 완전한 프라이빗이 필요하고 레거시 환경 지원 시
  • Private Fields (#): 모던 환경에서 진정한 프라이빗 필드가 필요할 때

브라우저 호환성 및 폴리필

브라우저 지원 현황

Symbol은 ES6(ES2015)에서 도입되어 모든 현대 브라우저에서 지원됩니다.

기능ChromeFirefoxSafariEdge
Symbol (기본)38+36+9+12+
Symbol.iterator38+36+9+12+
Symbol.for/keyFor40+36+9+12+
Well-known Symbols47+36+9+12+
Symbol.description70+63+12.1+79+

폴리필 고려사항

Symbol은 완전한 폴리필이 불가능합니다. Symbol의 핵심 기능인 “고유성”을 다른 원시 타입으로 구현할 수 없기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 부분적 폴리필 (완전하지 않음)
if (!window.Symbol) {
  window.Symbol = (function() {
    let counter = 0;

    function Symbol(description) {
      if (this instanceof Symbol) {
        throw new TypeError('Symbol is not a constructor');
      }

      const sym = Object.create(Symbol.prototype);
      sym._description = description;
      sym._id = `@@symbol_${++counter}_${Math.random().toString(36).slice(2)}`;
      return sym;
    }

    Symbol.prototype.toString = function() {
      return `Symbol(${this._description || ''})`;
    };

    // Symbol.for, Symbol.keyFor 등 추가 구현 필요...

    return Symbol;
  })();
}

레거시 환경 지원이 필요하다면 core-js 폴리필을 사용하되, 완전한 기능은 기대하지 마세요.

1
npm install core-js
1
import 'core-js/features/symbol';

실전 예제: 완전한 구현

플러그인 시스템 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 플러그인 시스템의 핵심 Symbol들
const PLUGIN_INIT = Symbol('plugin.init');
const PLUGIN_DESTROY = Symbol('plugin.destroy');
const PLUGIN_META = Symbol('plugin.meta');

class PluginManager {
  #plugins = new Map();

  register(Plugin) {
    const instance = new Plugin();
    const meta = instance[PLUGIN_META] || { name: Plugin.name, version: '1.0.0' };

    if (typeof instance[PLUGIN_INIT] === 'function') {
      instance[PLUGIN_INIT]();
    }

    this.#plugins.set(meta.name, { instance, meta });
    console.log(`플러그인 등록: ${meta.name} v${meta.version}`);

    return this;
  }

  unregister(name) {
    const plugin = this.#plugins.get(name);
    if (plugin && typeof plugin.instance[PLUGIN_DESTROY] === 'function') {
      plugin.instance[PLUGIN_DESTROY]();
    }
    this.#plugins.delete(name);
  }

  *[Symbol.iterator]() {
    yield* this.#plugins.values();
  }
}

// 플러그인 예시
class LoggerPlugin {
  get [PLUGIN_META]() {
    return { name: 'Logger', version: '2.0.0' };
  }

  [PLUGIN_INIT]() {
    console.log('Logger 플러그인 초기화됨');
    this.startTime = Date.now();
  }

  [PLUGIN_DESTROY]() {
    console.log(`Logger 플러그인 종료 (실행시간: ${Date.now() - this.startTime}ms)`);
  }

  log(message) {
    console.log(`[LOG] ${message}`);
  }
}

const manager = new PluginManager();
manager.register(LoggerPlugin);

for (const { meta } of manager) {
  console.log(`활성 플러그인: ${meta.name}`);
}

정리

Symbol은 JavaScript의 7번째 원시 타입으로, 고유하고 변경 불가능한 값을 생성합니다. 주요 특징과 활용법을 정리하면 다음과 같습니다.

핵심 개념:

  • Symbol()은 항상 고유한 값을 생성 (같은 설명이라도 다름)
  • Symbol.for()는 전역 레지스트리에서 Symbol을 공유
  • Symbol 프로퍼티는 열거에서 제외되고 JSON 직렬화에서 무시됨

Well-known Symbols:

  • Symbol.iterator: 객체를 이터러블하게 만듦
  • Symbol.toStringTag: 객체 타입 문자열 커스터마이징
  • Symbol.hasInstance: instanceof 동작 커스터마이징
  • Symbol.toPrimitive: 타입 변환 동작 제어
  • Symbol.species: 파생 객체 생성자 지정

실전 활용:

  • 프라이빗 프로퍼티 구현 (준 프라이빗)
  • 라이브러리 간 프로퍼티 충돌 방지
  • 커스텀 이터레이터 구현
  • 메타프로그래밍 및 내장 동작 커스터마이징

Symbol을 이해하면 JavaScript의 고급 패턴과 라이브러리 내부 구조를 더 깊이 이해할 수 있습니다. Proxy와 Reflect와 함께 사용하면 더욱 강력한 메타프로그래밍이 가능합니다. 자세한 내용은 JavaScript Proxy와 Reflect 완벽 가이드를 참고하세요.


참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.