— Map, Javascript — 1 min read
해쉬 자료구조를 공부하면서, Map 을 사용해서 구현해 봤습니다. 정말 Map을 사용해도되는 건지, 공부해보면서, 해쉬맵에는 Map을 쓰는 게 좋다는 걸 알게 되었어요!
MDN과 이를 기반한 해외포스팅을 읽고 정리해봤습니다.
아래 글에서 소개하는 Map의 성능에 대해서 객체와 비교 테스트를 해본 결과는 아래에 있습니다.
Map
객체는 키-값 쌍을 저장하며 각 쌍의 삽입 순서도 기억하는 콜렉션입니다. 아무 값(객체와 원시 값)이라도 키와 값으로 사용할 수 있습니다. - MDN
맵은 프로그래밍을 하면서 가장 자주 사용하는 자료구조입니다. Java에서 HashMap을 위해 Map이 사용되죠. 그러나 Javascript에서는 그걸 위해서는 그냥 객체를 쓰는게 꽤 편하죠.
1const map = {};23// 키-값 쌍 추가하기4map['key1'] = 'value1'5map['key2'] = 'value2'6map['key3'] = 'value3'78if (map.hasOwnProperty('key1')) {9 console.log('Map constains key1')10}1112console.log(map['key1']);
근데 진짜 맵을 쓰고 싶을 때를 위해서 자바스크립트에 내장된 자료구조가 있습니다. Map이죠. 객체보다 Map이 나은 이유를 소개드립니다.
객체의 키는 string과 symbol 타입만 가능합니다. 맵은 object
, function
, primitive 타입이 키로 사용될 수 있습니다.
1const map = new Map();2const myFunction = ()=>{}; // 유틸 함수, 함수형 컴포넌트일 수도 있겠죠? 함수라면!3const myNumber = 987;4const myObject = {5 name: '플레인한 프로퍼티 값',6 otherKey: '값'7}89map.set(myFunction, '내 키는 함수!')10map.set(myNumber, '내 키는 숫자')11map.set(myObject, '내 키는 객체')1213map.get(myFunction) // '내 키는 함수!'
맵은 size
프로퍼티를 제공하는 반면, 일반 객체의 크기 size
는 표현하기 까다로울 뿐 아니라 비효율적인 연산이 표함됩니다.
1const map = new Map();2map.set('someKey1', 1);3map.set('someKey2', 1);4...5map.set('someKey100', 1);67console.log(map.size) // 100, Runtime: O(1)89const plainObjMap = {};10plainObjMap['someKey1'] = 1;11plainObjMap['someKey2'] = 1;12...13plainObjMap['someKey100'] = 1;1415console.log(Object.keys(plainObjMap).length) // 100, Runtime: O(n)
맵은 항목의 빈번한 추가/삭제에 대해 최적화되어 있습니다.
2에서 본 바와 같이 Map
의 크기는 O(1)
시간에 가능한 반면, 객체의 사이즈를 연산할 때는 O(n)
단계를 거쳐야하죠. (*엄청난 차이입니다!)
또한, 모든 키를 string으로 바꿀 필요는 없기 때문에, 시간이 절약됩니다.
객체는 key들을 순회하면서 읽어오고 그걸 가지고 값을 순회하면서 읽어와야합니다. 반면에, 맵은 이터러블이기 때문에, 직접 순회가능합니다.
1const map = new Map();23for (let [key, value] of map) {4 `키와 값은 매 순회에서 배열 [${key},${value}]로 반환됩니다.5 배열 디스트럭쳐링 할당으로 위와 같이 표현할 수 있습니다.`6}
Map
은 키가 삽입된 순서대로 저장되는 것이 보장됩니다. ECMAScript 2015 (ES6) 전에는 객체의 키들의 순서는 보장되지 않았습니다. 이제는 객체의 키인 문자열과 Symbol
도 키의 생성 순서를 유지합니다.
일반 객체는 그것의 프로토타입들 때문에 이미 키를 가지고 있습니다. 이렇게 객체가 이미 가진 프로퍼티 키들과 충돌 가능성이 있는 거죠. Map
이 생성하는 객체는 초기화될 때, 어떤 키도 가지지 않습니다.
Object.create(null)
로 실수로 발생할 수 있는 키 오버라이딩을 피할 수 는 있습니다.1const map = new Map();2map.set('someKey1', 1);3map.set('someKey2', 2);4map.set('toString', 3); // 맵에서는 문제없죠.56const plainObjMap = {};7plainObjMap['someKey1'] = 1;8plainObjMap['someKey2'] = 2;9plainObjMap['toString'] = 3; // 워우, 이건 native 함수이름인데;
1map sets: 37.654ms2map gets: 18.902ms3map : getSize: 0.01ms4plain obj : sets: 1.254s5plain obj : gets: 607.29ms6plain obj : getSize: 254.167ms
1const plainObjMap = {};2const map = new Map();34console.time("map sets");5for (let i = 0; i < 100_000; i++) map.set(`someKey${i}`, i);6console.timeEnd("map sets");78console.time("map gets");9for (let [key, _] of map) map.get(key);10console.timeEnd("map gets");1112console.time("map : getSize");13map.size; // 100000, Runtime: O(1)14console.timeEnd("map : getSize");15/////1617console.time("plain obj : sets");18for (let i = 0; i < 1_000_000; i++) plainObjMap[`someKey${i}`] = i;19console.timeEnd("plain obj : sets");2021console.time("plain obj : gets");22for (let i = 0; i < 1_000_000; i++) plainObjMap[`someKey${i}`];23console.timeEnd("plain obj : gets");2425console.time("plain obj : getSize");26Object.keys(plainObjMap).length; // 100000, Runtime: O(n)27console.timeEnd("plain obj : getSize");
hash maps를 구현한다고 객체로 workaround하는 시대는 지났습니다. 객체는 정말 유용하지만, 전형적인 hash map을 구현할 때는 가장 좋은 선택은 아닐겁니다.
내장 생성자 함수인 Map이 가진 강점을 이해했으니, 어떤 자료구조를 구현하는 지에 따라 객체보다 나은 경우를 고려해서 써야겠습니다. 뒤늦게 알게 되었지만 멋진 내장 객체네요!