본문 바로가기

Symbol, Map, Set

고유 키인 Symbol, 임의 타입 키를 갖는 Map, 중복을 제거하는 Set의 자료 구조와 순회 방식을 익힙니다.

우리는 8장 초반에 let, const, 화살표 함수, 구조 분해 할당, 스프레드 연산자를 통해 문법 현대화를 확인했습니다.

ES2015(ES6)는 문법뿐 아니라 데이터 타입과 컬렉션 객체에도 중요한 변화를 가져왔습니다.

이번 장에서는 ES2015에 추가된 원시 타입 Symbol과 컬렉션 객체 Map, Set을 중심으로 데이터 구조화 방식을 정리합니다.


Symbol: 고유하고 유일한 값

Symbol은 ES2015(ES6)에서 새로 추가된 원시(Primitive) 타입입니다.

Symbol 값은 유일하고 변경 불가능하며(immutable), 주로 객체 프로퍼티의 키로 사용되어 이름 충돌 위험 없이 고유한 프로퍼티를 만들 때 활용됩니다.

Symbol 생성하기

Symbol() 함수를 호출하여 생성합니다.

이때 new 키워드를 사용하지 않습니다.

const mySymbol1 = Symbol();
const mySymbol2 = Symbol();

console.log(mySymbol1 === mySymbol2); // 결과: false (항상 유일한 값)

// 설명을 추가할 수 있습니다 (디버깅 용이)
const id = Symbol('id');
const id2 = Symbol('id'); // 설명이 같아도 심볼 값은 다릅니다.
console.log(id === id2); // 결과: false

설명은 심볼의 값을 정의하는 데 사용되지 않고, 디버깅할 때만 유용합니다.

객체 프로퍼티 키로 Symbol 사용하기

Symbol의 주된 용도는 객체의 프로퍼티 키로 사용하는 것입니다.

Symbol로 생성된 키는 다른 어떤 문자열 키나 다른 Symbol 키와도 충돌하지 않는 고유성을 보장합니다.

const userId = Symbol('user_id');
const userRole = Symbol('user_role');

const user = {
    name: "Alice",
    [userId]: 1234, // Computed property name (계산된 프로퍼티 이름) 문법 사용
    [userRole]: "admin"
};

console.log(user.name);    // 결과: Alice
console.log(user[userId]); // 결과: 1234
console.log(user[userRole]); // 결과: admin

// 다른 곳에서 동일한 설명으로 심볼을 만들어도 충돌하지 않습니다.
const anotherUserId = Symbol('user_id');
user[anotherUserId] = 5678; // 새로운 고유한 프로퍼티가 추가됩니다.
console.log(user[anotherUserId]); // 결과: 5678
console.log(user[userId]);       // 결과: 1234 (기존 userId와 다름)

Symbol 키의 특징: 숨겨진 프로퍼티

Symbol로 정의된 객체 프로퍼티는 일반적인 for...in 루프나 Object.keys(), Object.values(), Object.entries() 메서드로는 열거되지 않습니다.

const myObj = {
    name: "Bob",
    age: 40,
    [Symbol('secret')]: "비밀 데이터"
};

for (let key in myObj) {
    console.log(key); // name, age (secret 심볼은 출력되지 않음)
}

console.log(Object.keys(myObj));    // 결과: ["name", "age"]
console.log(Object.getOwnPropertyNames(myObj)); // 결과: ["name", "age"]

// Symbol 키를 얻는 방법
console.log(Object.getOwnPropertySymbols(myObj)); // 결과: [Symbol(secret)]
// 모든 프로퍼티 키(문자열 + 심볼)를 얻는 방법
console.log(Reflect.ownKeys(myObj)); // 결과: ["name", "age", Symbol(secret)]

여기서 “숨겨진”은 일부 열거 API가 Symbol 키를 제외한다는 뜻입니다. Reflect.ownKeys()나 Object.getOwnPropertySymbols()로 찾을 수 있으므로 비밀 정보나 접근 제어를 보장하는 수단은 아닙니다.

전역 Symbol 레지스트리

Symbol.for(key) 메서드를 사용하면 전역 Symbol 레지스트리(global Symbol registry)에 등록된 Symbol 값을 가져오거나 새로 생성할 수 있습니다.

Symbol.for()로 생성된 Symbol은 키 값을 공유하므로, 같은 키로 호출하면 항상 동일한 Symbol을 반환합니다.

const globalId1 = Symbol.for('shared_id');
const globalId2 = Symbol.for('shared_id');

console.log(globalId1 === globalId2); // 결과: true (동일한 Symbol)

const localId = Symbol('local_id'); // Symbol()로 생성된 심볼은 전역 레지스트리에 등록되지 않음

console.log(Symbol.keyFor(globalId1)); // 결과: "shared_id" (전역 레지스트리에서 키 가져오기)
console.log(Symbol.keyFor(localId));   // 결과: undefined (전역 레지스트리에 없으므로)

전역 레지스트리의 Symbol은 여러 모듈이나 프레임워크 간에 특정 Symbol을 공유해야 할 때 유용합니다.


Map: 순서가 있는 키-값 컬렉션

이름 있는 레코드, 임의 키로 연결하는 값, 중복 없는 집합 중 필요한 관계를 먼저 정합니다. 아래 표는 전체 열거와 객체 수명 관리까지 비교합니다.

Object·Map·Set·Weak 컬렉션 선택

DECISION CRITERIA

Object·Map·Set·Weak 컬렉션 선택

값의 집합인지 키와 값의 연결인지 구분한다. 열거와 size가 필요하면 일반 컬렉션을, 객체 수명에 묶는 보조 정보에는 약한 컬렉션을 검토한다.

데이터 관계와 열거 요구를 먼저 정하고, 약한 참조가 필요한지 확인한다.
컬렉션선택 조건키·값 범위열거 계약주의점
Set중복 없는 값 · 전체 열거 필요모든 값삽입 순서로 열거 · size객체는 내용 대신 참조 동일성으로 비교
WeakSet객체·비등록 Symbol의 보조 포함 여부객체 / 비등록 Symbol전체 열거·size 없음항목을 강하게 붙잡지 않음 · GC 시점 보장 없음
Map임의 키로 값 연결 · 전체 열거 필요모든 키삽입 순서로 열거 · size객체 키를 그대로 사용 · JSON 변환은 직접 설계
WeakMap객체·비등록 Symbol 수명에 메타데이터 연결객체 / 비등록 Symbol 키전체 열거·size 없음키의 수거를 방해하지 않음 · 값은 임의 타입
Object이름 있는 필드로 레코드 표현문자열 / Symbol 키열거 API별 범위 다름JSON은 Symbol 키를 제외 · Map과 순서 규칙이 다름

Set

선택 조건 중복 없는 값 · 전체 열거 필요

키·값 범위 모든 값

열거 계약 삽입 순서로 열거 · size

주의점 객체는 내용 대신 참조 동일성으로 비교

WeakSet

선택 조건 객체·비등록 Symbol의 보조 포함 여부

키·값 범위 객체 / 비등록 Symbol

열거 계약 전체 열거·size 없음

주의점 항목을 강하게 붙잡지 않음 · GC 시점 보장 없음

Map

선택 조건 임의 키로 값 연결 · 전체 열거 필요

키·값 범위 모든 키

열거 계약 삽입 순서로 열거 · size

주의점 객체 키를 그대로 사용 · JSON 변환은 직접 설계

WeakMap

선택 조건 객체·비등록 Symbol 수명에 메타데이터 연결

키·값 범위 객체 / 비등록 Symbol 키

열거 계약 전체 열거·size 없음

주의점 키의 수거를 방해하지 않음 · 값은 임의 타입

Object

선택 조건 이름 있는 필드로 레코드 표현

키·값 범위 문자열 / Symbol 키

열거 계약 열거 API별 범위 다름

주의점 JSON은 Symbol 키를 제외 · Map과 순서 규칙이 다름

약한 참조 ↔ 전체 열거 포기

WeakMap·WeakSet은 객체와 비등록 Symbol을 약하게 보유한다. 전체 목록·항목 수·GC 완료 시점을 관찰할 수 없으므로 캐시 만료 시각을 보장하는 도구로 쓰지 않는다.

Map은 ES2015(ES6)에서 도입된 새로운 컬렉션 객체로, 키-값(key-value) 쌍을 저장합니다.

일반 객체({})와 유사하지만, Map은 다음과 같은 중요한 차이점을 가집니다.

  • 키의 타입: Map의 키는 모든 데이터 타입(원시 값, 객체, 함수, 심볼 등)이 될 수 있습니다. 일반 객체의 프로퍼티 키는 문자열 또는 Symbol이며, 숫자로 접근해도 문자열 키로 변환됩니다.
  • 삽입 순서 보장: Map은 요소들의 삽입 순서를 기억하고 보장합니다. 일반 객체도 순서 규칙이 있지만, 배열 인덱스 형태의 문자열 키는 숫자 오름차순으로 앞서 나옵니다. 열거 API마다 포함하는 자체·상속·문자열·Symbol 범위도 다릅니다.
  • 성능: 대량의 키-값 쌍을 추가하거나 제거할 때 일반 객체보다 성능이 더 좋을 수 있습니다. 성능은 엔진·자료 크기·작업에 따라 측정해야 하며, 모든 작업에서 Map이 빠르다고 일반화하지 않습니다.
  • 크기(size): Map.prototype.size 프로퍼티를 통해 요소의 개수를 쉽게 얻을 수 있습니다. 일반 객체는 Object.keys().length와 같이 복잡한 방법으로 개수를 세어야 합니다.

Map 생성 및 기본 메서드

new Map()으로 생성하고 set(), get(), has(), delete(), clear() 등의 메서드와 size 프로퍼티를 사용합니다.

const myMap = new Map();

// 요소 추가: set(key, value)
myMap.set('name', 'Alice');
myMap.set(1, 'One');
myMap.set(true, 'Truth');
const objKey = { a: 1 };
myMap.set(objKey, 'This is an object key');

// 요소 가져오기: get(key)
console.log(myMap.get('name')); // 결과: Alice
console.log(myMap.get(1));    // 결과: One
console.log(myMap.get(objKey)); // 결과: This is an object key

// 요소 존재 여부 확인: has(key)
console.log(myMap.has('name')); // 결과: true
console.log(myMap.has('city')); // 결과: false

// 요소 개수: size
console.log(myMap.size); // 결과: 4

// 요소 삭제: delete(key)
myMap.delete(true);
console.log(myMap.size); // 결과: 3

// 모든 요소 삭제: clear()
myMap.clear();
console.log(myMap.size); // 결과: 0

Map 순회하기

for...of 루프를 사용하여 Map의 키, 값, 또는 엔트리(키-값 쌍)를 순회할 수 있습니다.

const userRoles = new Map([
    ['admin', '관리자'],
    ['editor', '편집자'],
    ['viewer', '시청자']
]);

// 1. Map.prototype.keys(): 키를 순회
for (let key of userRoles.keys()) {
    console.log(key); // admin, editor, viewer
}

// 2. Map.prototype.values(): 값을 순회
for (let value of userRoles.values()) {
    console.log(value); // 관리자, 편집자, 시청자
}

// 3. Map.prototype.entries() 또는 기본 순회: [key, value] 쌍을 순회
for (let entry of userRoles.entries()) {
    console.log(entry); // ['admin', '관리자'], ['editor', '편집자'], ['viewer', '시청자']
}
// 또는 구조 분해 할당과 함께 사용
for (let [role, description] of userRoles) { // userRoles는 기본적으로 entries() 이터레이터를 반환
    console.log(`${role}: ${description}`);
}
/*
결과:
admin: 관리자
editor: 편집자
viewer: 시청자
*/

Set: 중복 없는 값 컬렉션

Set은 ES2015(ES6)에서 도입된 새로운 컬렉션 객체로, 중복되지 않는 유일한 값들을 저장합니다.

배열과 유사하지만, Set의 모든 요소는 고유해야 한다는 중요한 제약이 있습니다.

Set 생성 및 기본 메서드

new Set()으로 생성하고 add(), has(), delete(), clear() 등의 메서드와 size 프로퍼티를 사용합니다.

const mySet = new Set();

// 요소 추가: add(value)
mySet.add(1);
mySet.add(5);
mySet.add('text');
mySet.add(1); // 중복 값은 무시됩니다.

console.log(mySet); // 결과: Set(3) {1, 5, "text"}

// 요소 존재 여부 확인: has(value)
console.log(mySet.has(1));    // 결과: true
console.log(mySet.has(10));   // 결과: false

// 요소 개수: size
console.log(mySet.size); // 결과: 3

// 요소 삭제: delete(value)
mySet.delete(5);
console.log(mySet.size); // 결과: 2 (1, "text"만 남음)

// 모든 요소 삭제: clear()
mySet.clear();
console.log(mySet.size); // 결과: 0

Set 순회하기

for...of 루프를 사용하여 Set의 요소들을 순회할 수 있습니다.

Set의 keys()와 values()는 각각 값을 순회하고, entries()는 [값, 값] 쌍을 순회합니다.

const uniqueNumbers = new Set([10, 20, 30, 20, 40]);
console.log(uniqueNumbers); // Set(4) {10, 20, 30, 40}

for (let num of uniqueNumbers) {
    console.log(num); // 10, 20, 30, 40
}

// Array.from()을 사용하여 Set을 배열로 변환
const numArray = Array.from(uniqueNumbers);
console.log(numArray); // 결과: [10, 20, 30, 40]

// 중복 제거 활용 예시
const mixedArray = [1, 2, 2, 3, 4, 4, 5, 'a', 'a', 'b'];
const uniqueMixedArray = [...new Set(mixedArray)]; // Set을 통해 중복 제거 후 스프레드 연산자로 배열 변환
console.log(uniqueMixedArray); // 결과: [1, 2, 3, 4, 5, 'a', 'b']

WeakMap, WeakSet (참고)

Map과 Set에는 WeakMap과 WeakSet이라는 약한 참조(weak reference)를 사용하는 변형이 있습니다.

이들은 키(WeakMap)나 값(WeakSet)에 대한 참조가 약하므로, 해당 객체가 더 이상 참조되지 않을 경우 가비지 컬렉션의 대상이 될 수 있습니다.

  • WeakMap: 현행 표준에서는 키로 객체 또는 전역 Symbol 레지스트리에 등록되지 않은 Symbol을 허용하며, 키에 대한 참조가 약합니다. 다른 강한 참조가 없다면 키 객체의 수거를 막지 않지만, 수거 시점은 보장하지 않습니다. DOM 요소 캐싱 등에 유용합니다.
  • WeakSet: 현행 표준에서는 값으로 객체 또는 비등록 Symbol을 허용하며, 값에 대한 참조가 약합니다. 다른 강한 참조가 없다면 값의 수거를 막지 않지만, 수거 시점은 보장하지 않습니다.
let obj1 = { id: 1 };
let obj2 = { id: 2 };

const wm = new WeakMap();
wm.set(obj1, "데이터1");

// obj1이 더 이상 참조되지 않으면 WeakMap의 해당 엔트리도 사라짐
obj1 = null; // 이제 obj1 객체는 WeakMap에서 자동 제거될 수 있음 (GC 대상)

const ws = new WeakSet();
ws.add(obj2);

// obj2가 더 이상 참조되지 않으면 WeakSet에서 해당 요소도 사라짐
obj2 = null; // 이제 obj2 객체는 WeakSet에서 자동 제거될 수 있음 (GC 대상)

WeakMap과 WeakSet은 메모리 누수 방지에 도움이 될 수 있지만, size나 clear() 같은 메서드를 사용할 수 없는 등 제약이 있습니다.

DOM 요소와 부가 상태를 연결할 때도 컬렉션의 선택 기준은 키 타입, 중복 여부, 정리 방식으로 나뉩니다.


예시 객체 내용이 같다는 판단과 같은 객체라는 판단을 구별해 봅시다. Map과 Set은 객체의 동일성을 비교하며, 원시 값은 SameValueZero 규칙을 사용합니다. 이 규칙에서는 NaN끼리 같고 +0과 -0도 같습니다.

const member = { id: 7 };
const visits = new Map([[member, 1]]);
console.log(visits.get({ id: 7 }));
console.log(visits.get(member));
console.log(new Set([NaN, NaN, +0, -0]).size);
visits.set(member, undefined);
console.log(visits.get(member), visits.has(member));

출력은 undefined, 1, 2, undefined true입니다. 새 { id: 7 }은 기존 키가 아니며, 저장된 undefined와 키 부재는 get()만으로 구별할 수 없습니다. 마지막 줄의 has()가 실제 키 존재를 확인합니다.

스스로 확인하기 new Map([["a", 1], ["b", 2]])에 set("a", 3)을 하면 키 순서는 어떻게 되나요? 이어서 delete("a"), set("a", 4)를 하면 어떤 순서인가요? [...map.keys()]로 확인하기 전에 예상해 보세요.

풀이 확인

첫 순서는 a, b입니다. 기존 키의 값만 바꾸면 최초 삽입 위치가 유지됩니다. 삭제한 뒤 다시 추가하면 새 삽입이므로 b, a입니다. 문자열 키의 동일성과 삽입 기록이 함께 결정한 결과입니다.

Weak 컬렉션은 전체 목록과 size를 제공하지 않습니다. 다른 곳에 키를 배열로 보관해 목록을 만들면 그 배열이 강한 참조를 추가하므로 목적도 다시 확인해야 합니다. 약한 참조는 명시적 만료 시간이나 수거 완료 확인을 대신하지 않습니다.

키 비교와 순서의 근거는 MDN Map 설명, 약한 참조의 제약은 MDN WeakMap 설명에 연결됩니다.

Symbol, Map, Set 정리

이번 장에서는 ES2015(ES6)에서 도입된 데이터 타입과 컬렉션 객체인 Symbol, Map, Set을 학습했습니다.

Symbol은 고유한 프로퍼티 키가 필요할 때 사용할 수 있습니다.

Map은 다양한 타입의 키와 삽입 순서를 다루고, Set은 중복 없는 값 목록을 다루며, WeakMap과 WeakSet은 약한 참조가 필요한 경우에 검토합니다.

Symbol, Map, Set, WeakMap, WeakSet은 키 충돌, 순서 보존, 중복 제거, 객체 수명 관리처럼 객체와 배열만으로 표현하기 어려운 요구사항을 다룰 때 사용합니다.

마지막으로 컬렉션을 선택할 때는 키의 타입, 중복 허용 여부, 객체 수명과 정리 방식을 함께 비교합니다.