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)]
이러한 특징 때문에 Symbol
은 프레임워크나 라이브러리에서 사용자 코드와 충돌하지 않는 내부적인 프로퍼티를 추가할 때 유용하게 사용됩니다.
전역 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: 순서가 있는 키-값 컬렉션
Map
은 ES2015(ES6)에서 도입된 새로운 컬렉션 객체로, 키-값(key-value) 쌍을 저장합니다. 일반 객체({}
)와 유사하지만, Map
은 다음과 같은 중요한 차이점을 가집니다.
- 키의 타입:
Map
의 키는 모든 데이터 타입(원시 값, 객체, 함수, 심볼 등)이 될 수 있습니다. 일반 객체의 키는 문자열(또는 Symbol)만 가능했습니다. - 삽입 순서 보장:
Map
은 요소들의 삽입 순서를 기억하고 보장합니다. 일반 객체는 ES2015부터 숫자 키를 제외하고는 삽입 순서를 보장하기 시작했지만, 여전히 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
: 키로 오직 객체만 허용하며, 키에 대한 참조가 약합니다. 키 객체가 가비지 컬렉션되면 WeakMap에서 해당 엔트리도 자동으로 제거됩니다. DOM 요소 캐싱 등에 유용합니다.WeakSet
: 값으로 오직 객체만 허용하며, 값에 대한 참조가 약합니다. Set의 요소가 가비지 컬렉션되면 WeakSet에서 해당 요소도 자동으로 제거됩니다.
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()
같은 메서드를 사용할 수 없는 등 제약이 있습니다.
마무리하며
이번 장에서는 ES2015(ES6)에서 새롭게 도입된 강력한 데이터 타입과 컬렉션 객체인 Symbol
, Map
, Set
에 대해 심도 있게 학습했습니다.
여러분은 Symbol
이 고유하고 유일한 값을 생성하여 객체의 프로퍼티 키로 사용될 때 이름 충돌을 방지하고 숨겨진 프로퍼티를 만들 수 있다는 것을 이해했습니다. 또한, Map
이 어떤 타입의 키라도 허용하며 삽입 순서를 보장하는 키-값 컬렉션임을, 그리고 Set
이 중복되지 않는 유일한 값들만을 저장하는 컬렉션임을 배웠습니다. 이 두 컬렉션의 주요 메서드와 for...of
를 이용한 순회 방법도 살펴보았습니다. 마지막으로, 약한 참조를 사용하는 WeakMap
과 WeakSet
의 개념도 간략하게 알아보았습니다.
이 새로운 데이터 타입과 컬렉션들은 자바스크립트 개발자가 데이터를 보다 유연하고 효율적으로 관리할 수 있도록 해줍니다. 기존 객체나 배열만으로는 해결하기 어려웠던 특정 문제들을 Symbol
, Map
, Set
을 통해 더 우아하게 해결할 수 있습니다. 다양한 상황에서 이들을 적절히 활용하여 코드의 품질을 높이는 연습을 계속하시길 바랍니다.