Object 대신 Map 사용을 고려해보자

·2023년 8월 15일
66

프론트엔드

목록 보기
2/12
post-thumbnail

📂 들어가기에 앞서

Map은 ES6 이후 도입된 자바스크립트의 고유한 내장 자료형(data-types)입니다. 구현 로직 특성상 Map을 사용하면 좋은 상황임에도 불구하고 Object에 비해 빈번하게 사용되지는 않다고 느껴졌습니다. Object와 비교하여 Map이 가진 특성과 이점들을 나열하고 어떤 상황에서 Map을 쓰면 좋을지 더 깊게 고민해보면 좋을 것 같다고 생각되어 글을 작성하게 되었습니다.

Map 의 이점

1. Map은 삽입 순서가 보장됩니다.

Map에서 키는 단순하고 직관적인 방식으로 정렬됩니다. Map 객체는 항목을 삽입한 순서대로 항목, 키 및 값을 반복합니다. 반면 Object는 내부적으로는 키-값 쌍을 기억하지만 순서를 보장하지는 않습니다.

2. Size를 구하는 메소드가 별도로 존재합니다.

const map = new Map();
map.set('키1', 1);
map.set('키2', 1);
...
map.set('키100', 1);

console.log(map.size) // 100, 실행시간: O(1)

Map 에는 size() 라는 메소드가 존재합니다. 다만 Object 는 열거 가능한 프로퍼티를 배열로 뽑아내는 Object.keys() 와 같은 메서드와 함께 .length 를 사용할 수는 있지만, 명확히 프로퍼티의 사이즈가 몇인지 구하는 메서드가 없으며, 프로퍼티의 사이즈를 구한다는 개념 자체도 애매합니다.

3. 여러 타입의 Key 값을 사용할 수 있습니다.

  const map = new Map();
  const object = {};

  // Map에 다양한 타입의 키 추가
  const key1 = { name: "key1" };
  const key2 = [1, 2, 3];
  const key3 = 42;

  map.set(key1, "Map value for key1");
  map.set(key2, "Map value for key2");
  map.set(key3, "Map value for key3");
  
  // Map
  console.log("Map:");
  console.log(map.get(key1)); // Map value for key1
  console.log(map.get(key2)); // Map value for key2
  console.log(map.get(key3)); // Map value for key3


  // Object에는 문자열 타입의 키만 추가 가능
  object["stringKey"] = "Object value for stringKey";
  console.log("Object:");
  console.log(object["stringKey"]); // Object value for stringKey
  //혹은 심볼 타입만 가능합니다.

};

4. Map은 빠릅니다.

이 글을 작성하기로 결심한 가장 큰 이유는 Map과 Object의 성능 비교때문이었습니다. 필자가 직접 set,get,delete 연산 코드를 작성하고 Map과 Object 속도를 비교해보았습니다.

[ set 성능 비교 ]

로직을 복사해서 직접 실행해보세요.

🖱️key 값을 문자열로 삽입했을때

const setFunction = () => {
  console.time("map test"); // 측정 시작
  const map = new Map();
  for (let index = 0; index < 100000; index++) {
    map.set('key'+index, index);
  }

  console.timeEnd("map test"); // 측정 종료

  console.time("object test"); // 측정 시작
  const object = {};

  for (let index = 0; index < 100000; index++) {
    object['key'+index] = index;
  }

  console.timeEnd("object test"); // 측정 종료
};

setFunction();


map test: 49.269ms
object test: 113.508ms

이처럼 map 과 Object 삽입 성능 속도를 측정해보니 map의 속도가 object보다 2배 이상 더 빨랐습니다.

🥇 set 로직 new Map승리 !

여기서 멈추시면 안됩니다 !

아주 중요한 사실이 남았습니다. 아래 코드에 삽입된 key 값 타입을 잘 봐주세요.

🖱️key 값을 숫자로 삽입했을때

console.time("map test"); // 측정 시작
const map = new Map();
for (let index = 0; index < 100000; index++) {
  map.set(index, index);
}

console.timeEnd("map test"); // 측정 종료

console.time("object test"); // 측정 시작
const object = {};

for (let index = 0; index < 100000; index++) {
  object[index] = index;
}

console.timeEnd("object test"); // 측정 종료

📈 set 결과 보기

map test: 15.206ms
object test: 4.58ms

해당 로직처럼 정수를 key값으로 삽입했을 때는 key 값을 문자열로 삽입했을때와 조금 다른 결과가 나왔습니다. (참고로 Object key 값은 문자열만 사용 가능하기 때문에 자연스럽게 문자열로 변환이 되어 출력됩니다. )

정수를 키로 하는 오브젝트의 경우 Object 가 더 빨랐습니다.

그렇다면 어떤 이유때문에 이렇게 동작하는걸까요 ?
...슬프지만 해당 사실에 대한 정확한 레퍼런스를 아직 찾지 못했습니다.

AI챗봇에게 물어봐서 얻은 답변을 공유합니다. Bing 이나 Chat Gpt 난 크로스 체킹이 필요한 부분이라고 생각하기 때문에 명확한 답이라고 확신 할 수 없습니다. 혹시 이유를 아시는분 있으면 댓글에 남겨주세요!

Bing의 답변

Chat Gpt의 답변

Map의 경우

내부적으로 해시 테이블을 사용하여 키를 저장하고 검색합니다. 해시 테이블은 키의 해시 값을 계산하고 해당 해시 값을 기반으로 데이터를 저장하고 검색하는 방식을 사용합니다. 그러나 정수를 키로 사용할 때는 이러한 해시 테이블에서의 연산 오버헤드가 발생할 수 있습니다. 해시 함수 계산 등이 추가되면서 키가 문자열이 아닐 경우 성능이 저하될 수 있습니다.

Object의 경우

정수 키를 직접 프로퍼티로 할당하는 방식을 사용합니다. 내부적으로 간단한 해시맵과 유사한 구조로 키-값 쌍을 관리하며 정수 키의 경우에도 문자열로 변환하는 과정이 필요하지 않습니다. 이로 인해 정수 키를 사용할 때는 Object가 조금 더 빠른 결과를 보일 수 있습니다.

[ get 성능 비교 ]

로직을 복사해서 직접 실행해보세요.

const getFunction = () => {
  const dataCount = 1000000;
  const map = new Map();
  const object = {};

  // 데이터 채우기
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    map.set(key, index);
    object[key] = index;
  }

  // Object get 성능 측정
  console.time("object test"); // 측정 시작
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    object[key];
  }
  console.timeEnd("object test"); // 측정 종료

  // Map get 성능 측정
  console.time("Map test"); // 측정 시작
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    map.get(key);
  }
  console.timeEnd("Map test"); // 측정 종료
};

getFunction();

📈 get 결과 보기

object test: 341.505ms
Map test: 34.484ms

실제 실험해본 결과, 무려 Object 에 비해 Map 의 get 속도가 10배나 빨랐습니다. 놀라운 결과 입니다.

🥇 get로직 new Map승리 !

[ Delete 성능 비교 ]

로직을 복사해서 직접 실행해보세요.

const deleteFunction = () => {
  const dataCount = 1000000;
  const map = new Map();
  const object = {};

  // 데이터 채우기
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    map.set(key, index);
    object[key] = index;
  }

  // Object delete 성능 측정
  console.time("object delete"); // 측정 시작
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    delete object[key];
  }
  console.timeEnd("object delete"); // 측정 종료

  // Map delete 성능 측정
  console.time("map delete"); // 측정 시작
  for (let index = 0; index < dataCount; index++) {
    const key = `key${index}`;
    map.delete(key);
  }
  console.timeEnd("map delete"); // 측정 종료
};

deleteFunction();

📈 Delete 결과 보기

object delete: 488.862ms
map delete: 384.821ms

🥇 Delete 로직 new Map승리!

delete 로직 또한 map 이 더 빠른 성능을 보였습니다.

실제로 Mdn문서에서는 키-값 쌍의 빈번한 추가 및 제거와 관련된 상황에서는 성능이 좀 더 좋다고 설명되어 있습니다. 블로그 글들을 찾아보니 이런 저런 논란이 많았지만 제가 직접 실험을 해 본 결과 저는 Mdn 설명이 정확하다는 판단을 내렸습니다. 정수로 키를 set 할 때는 결과가 조금 달랐지만 보통 로직을 구현하거나 데이터를 가공할 때는 명확한 문자열로 key 값을 사용하는 경우가 많으니까요.

이렇게 Map의 여러 이점들을 생각해보면 프로그램 성능 최적화를 위해 다음에는 Object 대신 Map사용을 시도해 보는 것도 좋은 방법이라고 생각합니다.

다만 이 글은 무조건적으로 "Map이 옳다! Map을 사용해야만 한다!!!!!😈😈😈" 라고 주장하는 글은 절대 아닙니다. 언제나 특정 기술을 사용할 때는 프로젝트의 특성을 잘 생각해봐야 합니다.

Map 객체는 객체의 프로퍼티를 자주 변경해야할 때 사용하는 것을 권장합니다. 기존 객체와 달리 순회도 쉽게 이루어져 데이터를 조작하기 쉽습니다.

하지만 문자열 기반 키만 사용하고 최대 읽기만 사용하는 경우, Object 더 나은 선택일 수 있습니다. 이는 Javascript 엔진이 백그라운드에서 Object C++ 클래스로 컴파일하고 속성에 대한 액세스 경로가 Map().get()에 대한 함수 호출보다 훨씬 빠르기 때문입니다.

결국 지금 내가 적용할 프로젝트에 가독성, 성능 등을 깊이 고려하여 가장 중요한 것이 무엇이 중요한지 판단을 내린 후에 적용 하는 것이 좋겠습니다.

더 나아가기

정확한 성능 측정을 위한 도구를 알아보기

console.time()과 console.timeEnd()는 정확한 시간 측정을 보장하지 않을 수 있습니다.

Note: console.time is non-standard and is not on a standards track, therefore do not use it on production. Only for development and test purposes.

정확한 시간 측정을 위해서는 더 정교한 성능 측정 도구를 사용해야 합니다. 다른 프로세스나 스레드의 영향, 메모리 상태 등 여러 가지 요소가 성능에 영향을 미칠 수 있습니다.

Non-standard

실제로 과거 Mdn 문서에 console 은 Non-standard 라고 정의하고 있습니다.

Non-standard
This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.

다만 console 가 Non-standard 라고 주장하는 블로그 레퍼런스들이 모두 6-7년전 글들이기 때문에 "과거"에 Mdn 문서에 이렇게 적혀있었다. 라고만 언급하겠습니다. 현재 Mdn 문서에는 발견 하지 못했습니다. 제가 못찾은걸 수도 있기 때문에 혹시 2023년 지금 이 시점에도 공식문서에 Non-standard 라는 정보가 존재한다면 댓글 달아주시면 감사하겠습니다.

No Production

console 은 development 용이지 production 용이 아닙니다. 이 점을 유의해야 합니다. 또한 코드의 실행이 빠르게 끝날 수 있는 작업이나 코드 블록이 여러 번 실행될 경우 정확한 시간 분석이 어려울 수 있습니다.

이러한 오류가 존재하는 이유는 console time의 측정 정밀도 때문입니다. console.time 은 밀리세컨드 단위로 시간을 측정합니다.

Performamce를 활용해보자

console.time 은 밀리세컨드 단위로 시간을 측정하는 반면, 퍼포먼스는 마이크로 초 단위의 정밀도를 가졌기 때문에 밀리 초 단위인 console 보다 performance 가 정밀도가 높습니다.

단위가 나와서 헷갈릴 수 있습니다. 충분이 이해합니다! 단순하게 얘기해보자면,

performance | 마이크로 세컨드
console.time | 밀리세컨드 
🌟 1밀리 세컨드 = 1000 마이크로 세컨드

이와 같은 단위로 시간을 측정한다는 얘기입니다. 마이크로 세컨드 단위로 시간을 측정하는 performance 를 사용해 더 정교하게 시간 단위를 측정할 수 있다는 결론이 나왔습니다.

사용 방법은 아주 쉽습니다. 퍼포먼스 메소드로 성능을 측정하기 위해서, 먼저 perf_hooks 를 import 합니다.

const { performance } = require("perf_hooks");

함수 만들어주기

function testMap() {
  const map = new Map();
  for (let index = 0; index < 100000; index++) {
    map.set("key" + index, index);
  }
}

function testObject() {
  const object = {};
  for (let index = 0; index < 100000; index++) {
    object["key" + index] = index;
  }
}

성능 측정하기


const mapStartTime = performance.now();
testMap();
const mapEndTime = performance.now();

console.log(`Map: ${mapEndTime - mapStartTime}ms`);


const objectStartTime = performance.now();
testObject();
const objectEndTime = performance.now();

console.log(`Object: ${objectEndTime - objectStartTime}ms`);

성능 측정할 함수를 시작하기 전 performance.now() 를 실행시킨 후 변수에 값을 담고(=mapStartTime) 측정 후 한번 더 함수를 실행 시켜 값을 구합니다.(=mapEndTime) mapEndTime 의 값에서 StartTime 을 빼면 보다 정확한 시간을 구할 수 있습니다.

자바스크립트 실행 시간은 브라우저나 사용자 컴퓨터에서 사용하는 엔진 등 여러 가지 요인에 의해 상당히 영향을 받을 수 있습니다. 성능 측정을 하는 이유는 근본적으로 정확하게 실행 속도를 분석하고 속도를 개선하기 위함이라고 생각합니다. 보다 확실한 속도 측정을 위해 performance.now 를 사용하는 것을 권장합니다.

최적화 시리즈

📈 [ JavaScript ] 깊은복사를 위한 함수 성능 측정과 특징 비교

📈 V8의 최적화 방식 히든클래스와 인라인 캐싱

Reference

console.time Mdn
https://developer.mozilla.org/en-US/docs/Web/API/Console/time

console은 왜 비표준인가요 ?
https://stackoverflow.com/questions/30269972/why-is-console-non-standard

https://ourcodeworld.com/articles/read/144/measuring-the-performance-of-a-function-with-javascript-using-browser-tools-or-creating-your-own-benchmark

http://vnthf.logdown.com/posts/2016/10/06/javascript
https://shanepark.tistory.com/220

https://shanepark.tistory.com/220

Map 의 이점을 더 알고싶다면
https://shanepark.tistory.com/220
이 글을 참고하세요.

profile
My Island

15개의 댓글

comment-user-thumbnail
2023년 8월 15일

좋은 글 너무 잘읽었습니다 !
정수를 키값으로 했을 때 뿐만 아니라 정수만으로 이루어진 문자열(ex : '1') 이 키값일 때도 Object가 더 빠르네요. 저도 이유를 찾고있는데 잘모르겠네요...ㅎㅎ
찾게되면 답글 남기겠습니다!

1개의 답글
comment-user-thumbnail
2023년 8월 15일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기
comment-user-thumbnail
2023년 8월 16일

우와! 유익한 정보 감사합니다! 🌞

1개의 답글
comment-user-thumbnail
2023년 8월 17일

오늘 알고리즘 풀이하며, 효율성 문제가 있었는데 이 글 보고 생각나서 Map 활용하니 바로 풀리더군요.. 복 많이 받으십쇼 응원합니다

1개의 답글
comment-user-thumbnail
2023년 8월 18일

performance를 사용했을 때의 소요시간 결과는 어떠셨을까요?
그리고 마지막 줄에 ferformance라고 오타가 있습니다~!

1개의 답글
comment-user-thumbnail
2023년 8월 18일

map에 대해서 공부하고 있었는데 이 글을 보고 빠르게 이해 했습니다. 감사합니다

1개의 답글
comment-user-thumbnail
2023년 8월 25일

예전에 순서 보장과 여러 종류의 타입을 key로 사용할 수 있다는 점에 매혹을 느껴 map에 관심이 생겨서 리서치한 적이 있었는데 반갑네요!
정수 키에서 object가 더 빠른 것은 아마도 javascript 엔진이 내부적으로 최적화하기 때문으로 알고 있습니다.
관련해서 스택오버플로우의 V8 개발자 답변을 참고하시면 좋을 것 같습니다. https://stackoverflow.com/a/62351925

이후 object도 실질적으로 순서가 보장되고, map의 typescript 지원이 제한적이라는 것 등을 알게 되면서 실무에선 안 쓰게 되었지만요...
정리해놨던 자료를 공유드리고 싶은데 어디로 갔는지 까먹었네요ㅠㅠ
좋은 글 감사드립니다!

1개의 답글