Object 대신에 Map을 더 많이 사용해보자!

우동이·2023년 2월 22일
6

JavaScript

목록 보기
1/9

1. 결론

자바스크립트의 객체를 사용할 때 내용의 변경이 발생하는 key-value 값을 저장하는 경우 일반 객체 대신 map을 활용하자!

Object

const object = {};

object["key"] = 'value';

delete object["key"];

Map

const map = new Map();

map.set('key', "value");

map.delete('key');

2. object 성능

  • 객체의 delete 연산자는 성능 저하로 이슈가 많지만 mapkey를 제거하는 데에 최적화가 되어 있어서 훨씬 빠르다고 합니다.
  • 위의 사진은 실제 성능 테스트를 한 결과이며 해당 링크에서 확인해볼 수 있습니다.
  • 해당 테스트는 마이크로 벤치마크 형태로 구현을 하였는데요. 이런 마이크로 벤치마크는 신뢰할 수 없다는 의견이 많다고 합니다. ( 해당 링크 참조 )
  • 그래서 이런 벤치마크를 신뢰하지 말고 다른 근거를 찾아보자!

3. map 성능

  • 실제 MDN 문서에는 mapkey를 자주 추가하거나 제거할 때 최적화가 되어있다는 것을 명확하게 설명하고 있습니다.
  • map 성능이 뛰어난 이유는 자바스크립트 VM 엔진이 object를 최적화 하는 방법과 관련이 있습니다.
    • 자바스크립트의 객체 성능 특징은 동적으로 key를 추가하거나 제거하는데 좋은 해시맵과 같은 구조에는 최적화되어있지 않다고 합니다.
  • 동적으로 변화하는 key-value에 대응하기 위해 map이 제작되었다고 합니다. ( hash 기반 )

4. object의 문제를 해결하는 유용한 map

4.1 내장된 key의 문제

  • 이미 자바스크립트의 객체는 내장된 수많은 key로 오염되어 있습니다.
  • 나중에 곤란한 버그가 발생할 수 있고 신경 써야 하는 부분이 증가합니다.
const myMap = {}

myMap.valueOf // => [Function: valueOf]
myMap.toString // => [Function: toString]
myMap.hasOwnProperty // => [Function: hasOwnProperty]
myMap.isPrototypeOf // => [Function: isPrototypeOf]
myMap.propertyIsEnumerable // => [Function: propertyIsEnumerable]
myMap.toLocaleString // => [Function: toLocaleString]
myMap.constructor // => [Function: Object]

4.2 반복문 적용

  • 객체는 아래와 같이 반복문을 사용할 때 이슈가 있습니다.
// 의도하지 않은 일부 상속된 키가 등장할 수 있다.
for (const key in myObject) {
  ...
}
  • 그래서 위와 같은 이슈를 해결하기 위해 보통 hasOwnProperty를 사용합니다.
for (const key in myObject) {
  if (myObject.hasOwnProperty(key)) {
    ...
  }
}
  • 극단적인 예시긴 하지만... 만약 사용자가 myObject.hasOwnProperty를 재정의 한다면?? 문제가 발생할 수 있습니다!
  • 사용자가 정의한 myObject.hasOwnProperty를 사용하려면 아래와 같이 정의해야 합니다!
// 점점 더티 코드가 되어가는...
for (const key in myObject) {
  if (Object.prototype.hasOwnProperty.call(myObject, key) {
  }
}
  • 코드를 조금더 깔끔하게 하고 싶다면?
Object.keys(myObject).forEach(key => {
  ...
})
  • 하지만 map을 사용하면!!
// map은 이터레이터를 제공하기 때문에 of를 사용할 수 있으며 key와 value를 한번에 가져올 수 있습니다!
for (const [key, value] of myMap) {
	...
}

4.3 key 순서

  • map의 장점은 key의 순서를 유지합니다!
    • 아시다시피 자바스크립트의 객체는 key의 순서를 유지하지 않고 특정한 규칙으로 배치됩니다.
  • 그리고 정확한 순서로 직접 key를 분해할 수 있는 기능을 제공합니다.
    • 이를 통해 O(1)의 시간 복잡도를 가지는 LRU 캐시 구현도 가능 ( 다양한 사례에 적용 )
const [[firstKey, firstValue]] = myMap

4.4 복사

  • 객체도 사실 몇가지의 장점을 가지고 있습니다.
// 펼치기 연산자
const copied = {...myObject}
// 객체 복사
const copied = Object.assign({}, myObject)
  • 하지만 map도 손쉽게 복사가 가능합니다.
const copied = new Map(myMap)
  • 또한 structuredClone를 사용하여 map의 깊은 복사본을 손쉽게 만들 수 있습니다.
const deepCopy = structuredClone(myMap)

4.5 map과 object의 자유로운 변환

const myObj = Object.fromEntries(myMap)
  • Object.entries를 활용하여 객체에서 map으로도 변환할 수 있습니다.
const myMap = new Map(Object.entries(myObj))
  • 보통 map을 활용할 때 아래와 같이 key-value 형태로 직접 넣어서 생성합니다.
const myMap = new Map([['key', 'value'], ['keyTwo', 'valueTwo']])
  • 하지만 자유로운 변환이 가능하기 때문에 더 쉽게 객체 형태로 map을 생성할 수 있습니다.
const myMap = new Map(Object.entries({
  key: 'value',
  keyTwo: 'valueTwo',
}))
  • helper 함수를 만들면 더 쉽겠죠?
const makeMap = (obj) => new Map(Object.entries(obj))

const myMap = makeMap({ key: 'value' })

4.6 Key Type

  • 객체는 key값에 대한 타입이 문자열 또는 심볼로 제한이 있습니다.
    • 허용되지 않는 key값이 전달되면 내장된 toString 메소드로 형변환을 진행합니다.
  • 하지만 mapkey 타입은 제한이 없고 모든 타입을 key로 활용할 수 있습니다.
myMap.set({}, value)
myMap.set([], value)
myMap.set(document.body, value)
myMap.set(function() {}, value)
myMap.set(myDog, value)
  • 이것의 장점은 아래와 같이 특정 데이터를 객체와 바로 연결할 수 있습니다.
const metadata = new Map()

metadata.set(myDomNode, {
  internalId: '...'
})

metadata.get(myDomNode)
// => { internalId: '...' }
  • 이와 같이 임시 상태들을 DB 형태로 읽고 쓰기에 매우 유용합니다!
    • 하지만 map이 참조하고 있는 데이터들은 가비지 컬렉터가 수집하지 않아 메모리 누수가 발생할 수 있습니다.
const metadata = new Map()

metadata.set(myTodo, {
  focused: true
})

metadata.get(myTodo)
// => { focused: true }

4.7 WeakMap

  • 내부에서 참조하고 있는 객체에 대해 약한 참조를 설정하고 있어서 메모리 누수를 해결해 줍니다!
const metadata = new WeakMap()

// 해당 객체에 참조가 없으면 자동으로 myTodo가 map에서 제거 => 자동으로 가비지 컬렉터가 수집후 제거 
metadata.set(myTodo, {
  focused: true
})

4.8 다양한 메소드를 제공

map.clear() // map 전체 제거
map.size // map 사이즈 가져오기
map.keys() // map 모든 키의 이터레이터
map.values() // map 모든 값의 이터레이터

5. set

  • 항상 map과 세트로 언급되는 set도 일반적인 배열보다 우수한 성능을 가지고 있습니다.

  • 추가, 제거에 대해 배열보다 더 좋은 성능을 제공합니다. ( 해당 링크 참조 )

  • MDN 문서에서도 Set의 성능에 대한 내용을 공유하고 있습니다.

  • 마찬가지로 WeakSet도 존재하며 메모리 누수를 방지할 수 있습니다.

6. 그러면 언제 사용하는 것이 좋을까?

  • 고정된 key 를 바탕으로 구조화된 데이터의 경우 일반적인 객체를 사용합니다.
    • 고정된 key는 빠른 읽기 및 쓰기에 매우 최적화되어 있습니다.
// 구조화된 데이터
const event = {
  title: 'title',
  date: new Date()
}
  • key가 여러개 존재하고 key를 자주 추가하거나 제거가 발생할 경우 map을 활용합니다.
const eventsMap = new Map()
eventsMap.set(event.id, event)
eventsMap.delete(event.id)
  • 특정 요소의 순서가 중요하고 의도적으로 중복을 허용하는 배열이면 일반 배열을 사용합니다.
const myArray = [1, 2, 3, 2, 1]
  • 중복을 원하지 않고 순서가 중요하지 않다면 set를 활용합니다.
const set = new Set([1, 2, 3])

7. 참고

profile
아직 나는 취해있을 수 없다...

0개의 댓글

관련 채용 정보