Map vs Object
by EarlyHail
알고리즘 스터디를 하다가 Object로 풀면 시간초과가 나는데 Map으로 풀면 해결되는 문제가 나왔다.
이야기가 나온 김에 Map과 Object의 차이점을 정리하고 성능을 측정해보려 한다.
Map vs Object
| Map | Object | |
|---|---|---|
| 의도치 않은 키 | Map은 명시적으로 제공한 키 외에는 어떤 키도 가지지 않습니다. | Object는 프로토타입을 가지므로 기본 키가 존재할 수 있습니다. 주의하지 않으면 직접 제공한 키와 충돌할 수도 있습니다. |
| 키 자료형 | Map의 키는 함수, 객체 등을 포함한 모든 값이 가능합니다. | Object의 키는 반드시 String 또는 Symbol이어야 합니다. |
| 키 순서 | Map의 키는 정렬됩니다. 따라서 Map의 순회는 삽입순으로 이뤄집니다. | Object의 키는 정렬되지 않습니다. |
| 크기 | Map의 항목 수는 size 속성을 통해 쉽게 알아낼 수 있습니다. | Object의 항목 수는 직접 알아내야 합니다. |
| 순회 | Map은 순회 가능하므로, 바로 순회할 수 있습니다. | Object를 순회하려면 먼저 모든 키를 알아낸 후, 그 키의 배열을 순회해야 합니다. |
| 성능 | 잦은 키-값 쌍의 추가와 제거에서 더 좋은 성능을 보입니다. | 잦은 키-값 쌍의 추가와 제거를 위한 최적화가 없습니다. |
출처 : MDN-JavaScript-Map
1. 의도치 않은 키
const map = new Map();
map.set("key1", 1);
map.set("toString", 2);
console.log(map.get("toString")); // 2
console.log(map.toString()); // do what toString do
const map2 = new Map();
map2["key1"] = 1;
map2["toString"] = 2;
console.log(map2.get("toString")); // 2
console.log(map2.toString()); // error
const obj = {};
obj[] = 1;
obj["toString"] = 2;
console.log(obj["toString"]); // 2
console.log(obj.toString()); // error
출처 : Stop Using Objects as Hash Maps in JavaScript
Map의 set을 사용할 경우 제공한 키만을 사용한다. 따라서 toString같은 값도 키로 사용할 수 있다.
하지만 Map과 bracket([])을 사용할 경우 Object와 비슷한 현상이 일어난다. (사실 이 방법은 Map의 부적절한 사용이다.)
2. 키 자료형
const map = new Map();
const user1 = { name: "HojinLee", nickname: "EarlyHail" };
const user2 = { name: "HojinLee", nickname: "EarlyHail" };
const function1 = () => true;
const function2 = () => true;
map.set(user1, 1);
map.set(function1, 2);
console.log(map.get(user1)); // 1
console.log(map.get(user2)); // undefined
console.log(map.get(function1)); // 2
console.log(map.get(function2)); // undefined
const obj = {};
const user3 = { name: "HojinLee", nickname: "EarlyHail" };
const user4 = { name: "HojinLee2", nickname: "EarlyHail2" };
obj[user3] = 1;
obj[user4] = 2;
console.log(obj[user3]); // 2
console.log(obj[user4]); // 2
console.log(obj["[object Object]"]); // 2
Map은 function과 Object등을 키로 사용할 수 있다.
하지만 Object는 String과 Symbol만 사용할 수 있다.
예시에서 볼 수 있는것 처럼 Object를 키로 등록하면 "[object Object]"가 키로 등록이 된다.

3. 키 순서
ECMA2015(ES6)이후 Object도 키가 생성된 순서를 기억합니다.
const map = new Map();
map.set("b", 1);
map.set("c", 2);
map.set("a", 3);
console.log(map.keys()); // [Map Iterator] { 'b', 'c', 'a' }
const obj = {};
obj.b = 1;
obj.c = 2;
obj.a = 3;
console.log(Object.keys(obj)); // [ 'b', 'c', 'a' ]
4. 크기
const map = new Map();
map.set("b", 1);
map.set("c", 2);
map.set("a", 3);
console.log(map.size); // 3
const obj = {};
obj.b = 1;
obj.c = 2;
obj.a = 3;
console.log(Object.keys(obj).length); // 3
5. 순회
const map = new Map();
map.set("b", 1);
map.set("c", 2);
map.set("a", 3);
const iter = map.entries();
let mapValue = iter.next();
while (mapValue.done != true) {
const [key, value] = mapValue.value;
console.log(`${key}: ${value}`);
mapValue = iter.next();
}
for (let [key, value] of map) {
console.log(`${key}: ${value}`);
}
const obj = {};
obj.b = 1;
obj.c = 2;
obj.a = 3;
Object.keys(obj).forEach((key) => console.log(`${key}: ${obj[key]}`)); // 3
Object도 entries로 [key, value]를 배열로 받아올 수 있지만 entries의 polyfill은 key를 조회한 뒤 접근하는 것이다.
Map vs Object - 성능
길이가 20인 랜덤 문자열을 200만개 생성하여 key, value로 삽입 / 조회 테스트를 해보자
const randomKeys = [];
const randomValues = [];
for (let i = 0; i < size; i++) {
randomKeys[i] = makeid(20);
randomValues[i] = makeid(20);
}
const benchmarkMap = (size) => {
const map = new Map();
console.time("benchmarkMapSet");
for (let i = 0; i < size; i++) map.set(randomKeys[i], randomValues[i]);
console.timeEnd("benchmarkMapSet");
console.time("benchmarkMapGet");
for (let i = 0; i < size; i++) {
const x = map.get(randomKeys[i]);
}
console.timeEnd("benchmarkMapGet");
console.time("benchmarkMapDelete");
for (let i = 0; i < size; i++) {
map.delete(randomKeys[i]);
}
console.timeEnd("benchmarkMapDelete");
};
const benchmarkObj = (size) => {
const obj = {};
console.time("benchmarkObjSet");
for (let i = 0; i < size; i++) obj[randomKeys[i]] = randomValues[i];
console.timeEnd("benchmarkObjSet");
console.time("benchmarkObjGet");
for (let i = 0; i < size; i++) {
const x = obj[randomKeys[i]];
}
console.timeEnd("benchmarkObjGet");
console.time("benchmarkObjDelete");
for (let i = 0; i < size; i++) {
delete obj[randomKeys[i]];
}
console.timeEnd("benchmarkObjDelete");
};
benchmarkMap(size);
benchmarkObj(size);

Map이 Set은 약 2배, Get은 약 30~40 배 빠른 성능을 보였고 Delete는 약간 느린 성능을 보였다.
MDN은 Map은 잦은 키-값 쌍의 추가와 제거에서 더 좋은 성능을 보입니다.라고 하는데… 결과가 달라서 당황스럽다

size = 10일 경우 Object가 Set, Get에 더 빠른 성능을 보였다.
const size = 1000000;
const benchmarkMap = (size) => {
console.time("benchmarkMapSet");
const map = new Map();
for (let i = 0; i < size; i++) map.set(i, i);
console.timeEnd("benchmarkMapSet");
console.time("benchmarkMapGet");
for (let i = 0; i < size; i++) {
const x = map.get(i);
}
console.timeEnd("benchmarkMapGet");
console.time("benchmarkMapDelete");
for (let i = 0; i < size; i++) {
map.delete(i);
}
console.timeEnd("benchmarkMapDelete");
};
const benchmarkObj = (size) => {
console.time("benchmarkObjSet");
const obj = {};
for (let i = 0; i < size; i++) obj[i] = i;
console.timeEnd("benchmarkObjSet");
console.time("benchmarkObjGet");
for (let i = 0; i < size; i++) {
const x = obj[i];
}
console.timeEnd("benchmarkObjGet");
console.time("benchmarkObjDelete");
for (let i = 0; i < size; i++) {
delete obj[i];
}
console.timeEnd("benchmarkObjDelete");
};
benchmarkMap(size);
benchmarkObj(size);

연속된 key, value 같이 간단한 key, value의 경우 Object가 성능이 더 좋았다.
간단하게 사용할 때는 Object, 참조 수나 데이터 수가 많을 때는 Map을 사용하게 될것 같다.