Skip to content

domukLog

객체보다 Map이 더 나은 6가지 이유

Map, Javascript1 min read

HashMap을 구현할 때 Map을 써야하는 이유

해쉬 자료구조를 공부하면서, Map 을 사용해서 구현해 봤습니다. 정말 Map을 사용해도되는 건지, 공부해보면서, 해쉬맵에는 Map을 쓰는 게 좋다는 걸 알게 되었어요!

MDN과 이를 기반한 해외포스팅을 읽고 정리해봤습니다.


아래 글에서 소개하는 Map의 성능에 대해서 객체와 비교 테스트를 해본 결과는 아래에 있습니다.

cabinets orders by number

0. 맵 Map

Map 객체는 키-값 쌍을 저장하며 각 쌍의 삽입 순서도 기억하는 콜렉션입니다. 아무 값(객체와 원시 값)이라도 키와 값으로 사용할 수 있습니다. - MDN

은 프로그래밍을 하면서 가장 자주 사용하는 자료구조입니다. Java에서 HashMap을 위해 Map이 사용되죠. 그러나 Javascript에서는 그걸 위해서는 그냥 객체를 쓰는게 꽤 편하죠.

1const map = {};
2
3// 키-값 쌍 추가하기
4map['key1'] = 'value1'
5map['key2'] = 'value2'
6map['key3'] = 'value3'
7
8if (map.hasOwnProperty('key1')) {
9 console.log('Map constains key1')
10}
11
12console.log(map['key1']);

근데 진짜 을 쓰고 싶을 때를 위해서 자바스크립트에 내장된 자료구조가 있습니다. Map이죠. 객체보다 Map이 나은 이유를 소개드립니다.

1. 키의 타입 제한이 없습니다.

객체의 키는 string과 symbol 타입만 가능합니다. 맵은 object, function, primitive 타입이 키로 사용될 수 있습니다.

1const map = new Map();
2const myFunction = ()=>{}; // 유틸 함수, 함수형 컴포넌트일 수도 있겠죠? 함수라면!
3const myNumber = 987;
4const myObject = {
5 name: '플레인한 프로퍼티 값',
6 otherKey: '값'
7}
8
9map.set(myFunction, '내 키는 함수!')
10map.set(myNumber, '내 키는 숫자')
11map.set(myObject, '내 키는 객체')
12
13map.get(myFunction) // '내 키는 함수!'

2. 크기를 ''가볍게'' 알 수 있습니다

맵은 size 프로퍼티를 제공하는 반면, 일반 객체의 크기 size는 표현하기 까다로울 뿐 아니라 비효율적인 연산이 표함됩니다.

1const map = new Map();
2map.set('someKey1', 1);
3map.set('someKey2', 1);
4...
5map.set('someKey100', 1);
6
7console.log(map.size) // 100, Runtime: O(1)
8
9const plainObjMap = {};
10plainObjMap['someKey1'] = 1;
11plainObjMap['someKey2'] = 1;
12...
13plainObjMap['someKey100'] = 1;
14
15console.log(Object.keys(plainObjMap).length) // 100, Runtime: O(n)

3. 더 나은 성능

은 항목의 빈번한 추가/삭제에 대해 최적화되어 있습니다.

2에서 본 바와 같이 Map의 크기는 O(1) 시간에 가능한 반면, 객체의 사이즈를 연산할 때는 O(n) 단계를 거쳐야하죠. (*엄청난 차이입니다!)

또한, 모든 키를 string으로 바꿀 필요는 없기 때문에, 시간이 절약됩니다.

4. 객체와 달리 직접 순회할 수 있습니다.

객체는 key들을 순회하면서 읽어오고 그걸 가지고 값을 순회하면서 읽어와야합니다. 반면에, 맵은 이터러블이기 때문에, 직접 순회가능합니다.

1const map = new Map();
2
3for (let [key, value] of map) {
4 `키와 값은 매 순회에서 배열 [${key},${value}]로 반환됩니다.
5 배열 디스트럭쳐링 할당으로 위와 같이 표현할 수 있습니다.`
6}

5. 키 순서

Map은 키가 삽입된 순서대로 저장되는 것이 보장됩니다. ECMAScript 2015 (ES6) 전에는 객체의 키들의 순서는 보장되지 않았습니다. 이제는 객체의 키인 문자열과 Symbol도 키의 생성 순서를 유지합니다.

ECMAScript 201 == ECMAScript 2015 이후 == es6+

6. 키 오버라이딩 금지

일반 객체는 그것의 프로토타입들 때문에 이미 키를 가지고 있습니다. 이렇게 객체가 이미 가진 프로퍼티 키들과 충돌 가능성이 있는 거죠. Map이 생성하는 객체는 초기화될 때, 어떤 키도 가지지 않습니다.

  • ES6 부터는, Object.create(null)로 실수로 발생할 수 있는 키 오버라이딩을 피할 수 는 있습니다.
1const map = new Map();
2map.set('someKey1', 1);
3map.set('someKey2', 2);
4map.set('toString', 3); // 맵에서는 문제없죠.
5
6const plainObjMap = {};
7plainObjMap['someKey1'] = 1;
8plainObjMap['someKey2'] = 2;
9plainObjMap['toString'] = 3; // 워우, 이건 native 함수이름인데;

비교테스트

  • 테스트 결과 : 100,000개의 데이터에 대해, 저장, 읽기, 전체 크기 연산에 대해 5번 수행 결과 중 하나입니다.
  • 자료를 저장하는 속도해서는 2배 이상, 자료를 읽어오고, 크기를 연산하는 속도에서는 압도적인 차이입니다.
1map sets: 37.654ms
2map gets: 18.902ms
3map : getSize: 0.01ms
4plain obj : sets: 1.254s
5plain obj : gets: 607.29ms
6plain obj : getSize: 254.167ms
1const plainObjMap = {};
2const map = new Map();
3
4console.time("map sets");
5for (let i = 0; i < 100_000; i++) map.set(`someKey${i}`, i);
6console.timeEnd("map sets");
7
8console.time("map gets");
9for (let [key, _] of map) map.get(key);
10console.timeEnd("map gets");
11
12console.time("map : getSize");
13map.size; // 100000, Runtime: O(1)
14console.timeEnd("map : getSize");
15/////
16
17console.time("plain obj : sets");
18for (let i = 0; i < 1_000_000; i++) plainObjMap[`someKey${i}`] = i;
19console.timeEnd("plain obj : sets");
20
21console.time("plain obj : gets");
22for (let i = 0; i < 1_000_000; i++) plainObjMap[`someKey${i}`];
23console.timeEnd("plain obj : gets");
24
25console.time("plain obj : getSize");
26Object.keys(plainObjMap).length; // 100000, Runtime: O(n)
27console.timeEnd("plain obj : getSize");

마치며

hash maps를 구현한다고 객체로 workaround하는 시대는 지났습니다. 객체는 정말 유용하지만, 전형적인 hash map을 구현할 때는 가장 좋은 선택은 아닐겁니다.

내장 생성자 함수인 Map이 가진 강점을 이해했으니, 어떤 자료구조를 구현하는 지에 따라 객체보다 나은 경우를 고려해서 써야겠습니다. 뒤늦게 알게 되었지만 멋진 내장 객체네요!

major reference