JavaScript의 Map 자료구조에 대해서 자세하게 알아보자 Thumbnail

Map 내장 메서드 가지고 삽질했던 경험

15 min read
JavaScript자료구조

JavaScript의 new Map() 구조에 대해서 알아보자

썸네일썸네일

자바스크립트에는 객체라는 자료형이 존재한다. Object는 키-값을 쌍으로 데이터를 저장하는 자료형으로, 원시 타입과는 다르게 여러 값을 하나의 단위로 묶어서 관리할 수 있는 참조 타입 자료형이다.

기본적인 사용법은 아래와 같다.

const obj = {
    location: "Seoul"
};

obj.name = "jeongwoo"

console.log(obj);

/**
> console.log(obj);
{ 
    name: "jeongwoo",
    location: "Seoul"
} 
*/

자바스크립트 문법에서 가장 기초가 되는 객체 구조이다. 하지만 이 글에서는 이 객체에 대해서 다루고자 하는 것이 아닌 new Map() 구조와의 차이점을 중점적으로 비교해보고자 한다.

기본적인 차이점

Object, 객체 자료형은 데이터 저장도 하지만 원래 Object 자료형은 데이터 저장보다는 객체 지향 프로그래밍을 위해 설계되었다. 그렇지만 실제로는 키-값 저장소로 많이 사용되고 있다.

Map 구조는 똑같이 키-값 구조이지만, 이 자료구조는 정말 데이터 저장만을 목적으로 하는 점이 다르다.

Map() 에 대해서

The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.

기본적으로 new Map()으로 생성하는 map 자료구조는 객체와 유사하다.

MDNMDN

둘 다 값을 지정하는 키를 설정하고, 그에 대한 값을 저장한다.

Map과 Object의 차이점

차이점은 몇 가지가 있는데 그 중에서 내가 중요하다고 생각하는 성능과 반복에 대해서 더 자세하게 다뤄보겠다.

일반적인 차이점, 키의 타입과 순서 보장, 크기 확인, 프로토타입 오염 방지

Object와 Map 구조의 차이점 중 첫째는 키의 타입이다. Object의 경우에는 키 값에 string, symobl만 가능하다. 하지만 Map은 다른 타입들도 가능하다는 점이 다르다.

일반적으로 Object에 1이라는 키 값을 사용하면 "1" 같이 string 타입으로 자동 변환되는데, Map의 경우에는 number 타입의 1이 키 값이 될 수 있다.

const map = new Map();
map.set(1, 'number key'); 

순서 보장, 이건 공부하면서 새롭게 알게 된 사실이다. map 에 추가하는 데이터의 순서를 보장한다. 정확히 말하면 추가된 키 값의 순서를 보장한다고 한다.

map.set('b', 2);
map.set('a', 1);
console.log(map.keys()); // ['b', 'a']

크기 계산, Object에 들어있는 데이터의 키-값의 수를 알려면 Object.keys(obj).length 처럼 2번 연산을 해야한다. 하지만 Map의 경우에는 기본 내장 프로퍼티인 map.size로 쉽게 값을 받을 수 있다.

반복, Iteration

이 글을 쓰려고 했던 핵심 이유이다. Object와 Map 구조는 반복하는 방법에서 차이가 있다.

// Object: 메서드를 통해 간접적으로
const obj = { a: 1, b: 2 };
for (const key in obj) { }           // for...in
Object.keys(obj).forEach(key => {}); // 배열로 변환 후 순회

// Map: iterable이라 직접 가능
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) { }  // 바로 순회 가능
map.forEach((value, key) => {});     // forEach도 지원

성능 최적화

Object를 사용하는 것에 비해 Map 구조를 사용하면 얻을 수 있는 성능적인 이점이 있다.

일반적으로 값을 추가하는 것은 Map이 더 빠르다고 생각하면 된다. 아래에서 10만개의 데이터를 추가하는 테스트를 해봤을 때 생각보다 유의미한 차이가 있었다.

데이터 삽입 테스트

const iterations = 100000;

// Object 삽입 테스트
const obj = {};
console.time("Object insert");
for (let i = 0; i < iterations; i++) {
  obj[`key${i}`] = i;
}
console.timeEnd("Object insert");

// Map 삽입 테스트
const map = new Map();
console.time("Map insert");
for (let i = 0; i < iterations; i++) {
  map.set(`key${i}`, i);
}
console.timeEnd("Map insert");

console.log(`Object size: ${Object.keys(obj).length}`);
console.log(`Map size: ${map.size}`);
// 데이터가 100개일 경우
Object insert: 0.305ms
Map insert: 0.021ms
Object size: 100
Map size: 100
// 데이터가 10만 개일 경우
Object insert: 64.969ms
Map insert: 15.67ms
Object size: 100000
Map size: 100000

이번에는 삭제 테스트를 해보자.

아래는 Object에서 delete 키워드를 사용하는 것과 Map의 delete 내장 메서드를 사용하는 것의 속도 비교이다. 브라우저 콘솔을 켜서 코드를 복붙해서 테스트해 볼 수 있다.

const iterations = 100000;

// Object 삭제 테스트
const obj = {};
for (let i = 0; i < iterations; i++) {
  obj[`key${i}`] = i;
}

console.time('Object delete');
for (let i = 0; i < iterations; i++) {
  delete obj[`key${i}`];
}
console.timeEnd('Object delete');

// Map 삭제 테스트
const map = new Map();
for (let i = 0; i < iterations; i++) {
  map.set(`key${i}`, i);
}

console.time('Map delete');
for (let i = 0; i < iterations; i++) {
  map.delete(`key${i}`);
}
console.timeEnd('Map delete');

console.log(`Object size: ${Object.keys(obj).length}`);
console.log(`Map size: ${map.size}`);
Object delete: 30.201ms
Map delete: 24.388ms
Object size: 0
Map size: 0

추가도 Map이 더 빨랐지만, 삭제는 차이가 큰 걸 알 수 있었다.

왜 더 빠를까?

삭제나 추가에서 Map이 더 빠른 이유는 Object 구조만의 독특한 설계 때문이다. 이는 JavaScript의 V8엔진과 연관되어 있다. V8엔진은 내부적으로 히든 클래스를 만들어서 저장한다.

const obj = { a: 1, b: 2, c: 3 };

// V8 엔진이 내부적으로 이런 "shape"을 만듦
// HiddenClass1: { a: offset 0, b: offset 1, c: offset 2 }

히든 클래스를 사용해서 객체의 전체 데이터가 아닌 DB의 인덱스 느낌으로 전체적인 모양을 저장한다. 이런 식으로 데이터 추가가 되거나 삭제가 되면 다시 히든클래스를 생성하는 오버헤드가 존재하기 때문에 삽입, 삭제 연산에서 성능차이가 발생하는 것이다.

Map 구조는 단순히 해쉬맵 자료구조의 역할을 위해서 존재한다. 따라서 별도의 히든클래스를 만들어서 저장하지 않고 삭제시 단순히 연결을 끊어버리는 동작을 하기 때문에 더 빠르다.

Map의 내장 메서드

최근에 기업 코딩테스트를 보는데, MDN 레퍼런스가 제공되지 않는 환경에서 시험을 치뤘다. 해시맵 자료구조를 사용해야 하는데 Object 대신 Map 자료구조를 선택했다.

get, set 정도는 익숙해서 기억을 했지만, 전체 Map을 배열로 변환하는 방법이 기억이 나지 않아 메서드 찾기로 삽질을 오래했던 기억이 난다.

그래서 글을 작성하는 겸 메서드들을 숙지해보려고 한다. 물론 다 외울 필요는 없겠지만 핵심적인 메서드는 알아두는게 좋을 것 같아서.

메서드설명반환값예시
set(key, value)키-값 쌍 추가/수정Map 객체 (체이닝 가능)map.set('name', '정우')
get(key)키로 값 조회값 또는 undefinedmap.get('name')'정우'
has(key)키 존재 여부 확인booleanmap.has('name')true
delete(key)키-값 쌍 삭제boolean (삭제 성공 여부)map.delete('name')true
clear()모든 항목 삭제undefinedmap.clear()
keys()모든 키를 가진 이터레이터Iterator[...map.keys()]['a', 'b']
values()모든 값을 가진 이터레이터Iterator[...map.values()][1, 2]
entries()모든 [키, 값] 쌍 이터레이터Iterator[...map.entries()][['a',1], ['b',2]]
forEach(callback)각 항목마다 콜백 실행undefinedmap.forEach((v, k) => {})

이렇게 보면 어렵지 않은데 시험에서는 Object.entries(map) , Object.keys(map) 별걸 다 시도해봤었다.. ㅋㅋ

이렇게 시도한 이유는 일반적인 Object를 배열로 바꿀 때는 이런 식으로 사용했었기 때문에 당연히 지원이 될 줄 알았다.

const map = new Map();
const obj = {
  A: 3,
  B: 2,
  C: 1,
};
map.set("A", 3);
map.set("B", 2);
map.set("C", 1);

console.log(map);
console.log(Object.entries(map));
console.log(Object.entries(obj));

/**
Map(3) { 'A' => 3, 'B' => 2, 'C' => 1 }
-> Object.entries(map)은 빈배열 반환 []
[ [ 'A', 3 ], [ 'B', 2 ], [ 'C', 1 ] ]
*/

결국에는 메서드를 여러개 시도해보다가 되는 걸 찾아서 시험을 잘 마쳤지만.. 시험을 보고 다음 날 인턴 동료에게 들었던 충격적인 사실은 스프레드 연산자를 활용한 ...map으로 배열([ 'A', 3 ] [ 'B', 2 ] [ 'C', 1 ])로 변환된다는 사실이었다. 💀💀

console.log(Object.entries(Object.fromEntries(map))); 이런 방법도 있다. JS는 레퍼런스를 줘야한다고 생각..

그렇다면 무조건 Map이 더 좋은가?

성능을 보면 무조건 Map을 써야할 것처럼 보인다. 하지만 실제로 서비스를 만들거나 프로젝트를 하는 경우에서 Map을 잘 쓰지 않는다는 것을 알 수 있다.

Object는 JSON 직렬화를 쉽게 할 수 있고, 구현이 간단하다는 점, 구조 분해 할당이라는 강력한 문법 지원 등으로 인해 실질적으로는 Map 보다 더 많이 사용된다.

// Object - 편리함
const { name, age, email } = user;

// Map - 불가능
const name = userMap.get('name');
const age = userMap.get('age');
const email = userMap.get('email');

그럼 언제 써요?

Map 구조는 빈번한 데이터 추가와 삭제가 일어날 때 사용한다. 혹은 키 값으로 string이 아닌 다른 타입을 사용할 때 사용한다.

Object 구조는 API 응답 데이터, React의 Props나 State, 구조 분해 할당이 필요할 때 사용할 수 있다.

결론

생각보다 Map의 장점이 명확히지만, Object의 장점이 더 큰 것 같다. 많은 데이터가 들어가서 성능을 고려해서 Map을 쓰기에는 Object가 너무 편리하다는 점이 크다.

그럼에도 Map이 좋은 점은 조금 더 명시적이라는 점인 것 같다. get과 has를 사용해서 휴먼에러를 방지한다는 점이 장점이라고 생각한다.

Table of Contents

0
추천 글