개요

객체는 만능처럼 보이지만 모든 키-값 저장에 적합한 도구는 아님 키를 자주 추가·삭제하고 임의 키로 조회하는 해시맵 유사 패턴에서는 Map이 더 안전하고 일관된 성능을 제공함 중복 없는 집합 연산에는 Set이 자연스러운 선택지

언제 객체 대신 Map을 고려할까

  • 키를 자주 추가·삭제하는 동적 해시맵 패턴
  • 안전한 반복과 구조 분해가 필요한 경우
  • 키의 삽입 순서 유지가 중요한 경우
  • 비문자열 키가 필요한 경우
// 객체 기반 임의 키-값 해시맵 패턴은 delete에서 성능 불리할 수 있음
const mapOfThingsObj = {}
mapOfThingsObj[thing.id] = thing
delete mapOfThingsObj[thing.id]

// Map은 동적 추가·삭제에 최적화
const mapOfThings = new Map()
mapOfThings.set(thing.id, thing)
mapOfThings.delete(thing.id)

성능 배경

객체는 VM이 숨은 클래스/셰이프를 가정해 최적화하는 경향이 있어 구조가 변동하면 디옵티마이즈 유발 가능성 존재 Map은 해시맵 사용에 맞춰 키 추가·삭제가 빈번한 경우를 목표로 설계됨 마이크로벤치마크는 한계가 있으므로 실제 워크로드에서 측정 권장, 다만 Map이 해당 패턴에 맞춰 설계된 점은 문서로 확인 가능

참고 개념

  • 객체 셰이프 변화는 모노모픽 → 폴리모픽 전이 유발 가능
  • Map은 내부적으로 동적 키 공간에 최적화된 경로 제공

내장 키 오염 문제

객체는 프로토타입 체인에 의해 비어 있어도 여러 키가 이미 존재함

const myMapLikeObj = {}
myMapLikeObj.valueOf
myMapLikeObj.toString
myMapLikeObj.hasOwnProperty

임의 키-값 저장에서 우연히 충돌하거나 방어 코드가 필요해짐

반복의 일관성

for..in은 상속된 키까지 순회 가능해 방어 코드 필요해짐

for (const key in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
    // 안전하지만 장황함
  }
}

Object.keys(obj).forEach(key => {
  // 키만 순회
})

Map은 표준 이터레이터를 제공하며 키와 값을 구조 분해로 즉시 획득 가능

for (const [key, value] of myMap) {
  // 깔끔한 순회
}

키 순서 유지

Map은 삽입 순서를 보존함 정확한 순서로 구조 분해 가능

const [[firstKey, firstValue]] = myMap

LRU 캐시 같은 순서 기반 전략 구현에 유리함

복사

객체는 전개나 assign으로 얕은 복사 쉬움

const copyObj = { ...obj }
const copyObj2 = Object.assign({}, obj)

Map도 생성자에 이터러블을 넘겨 간단히 복사 가능

const copyMap = new Map(myMap)

깊은 복사는 structuredClone 사용 가능

const deepCopyMap = structuredClone(myMap)

Map ↔ 객체 상호 변환

Map → 객체

const objFromMap = Object.fromEntries(myMap)

객체 → Map

const mapFromObj = new Map(Object.entries(obj))

선언 시 객체 리터럴을 이용해 가독성 개선 가능

const myMap = new Map(Object.entries({
  key: 'value',
  keyTwo: 'valueTwo',
}))

// 간단 헬퍼
const makeMap = (o) => new Map(Object.entries(o))
const m = makeMap({ key: 'value' })

TypeScript 제네릭 형태

const makeMap = <V = unknown>(o: Record<string, V>) => new Map<string, V>(Object.entries(o))

키 타입 유연성과 WeakMap

객체 키는 문자열 또는 심볼로 제한됨 Map은 임의 객체를 키로 사용 가능

myMap.set({}, value)
myMap.set([], value)
myMap.set(document.body, value)
myMap.set(() => {}, value)
myMap.set(myObj, value)

메타데이터를 원본 객체에 오염 없이 부착하는 패턴에 유용함

const metadata = new Map()
metadata.set(myDomNode, { internalId: '...' })
metadata.get(myDomNode)

단점은 참조를 보유하면 가비지 컬렉션이 지연되어 메모리 누수 위험 존재 이 경우 WeakMap 사용

const metadata = new WeakMap()
metadata.set(myTodo, { focused: true })
// 다른 강한 참조가 사라지면 자동으로 수거됨

Map 유틸리티 메서드 요약

  • map.clear() 전체 비우기
  • map.size 크기 조회
  • map.keys() 키 이터레이터
  • map.values() 값 이터레이터

Set과 WeakSet

Set은 고유 요소 집합 표현에 적합하며 추가·삭제·조회가 단순하고 종종 배열 대비 성능상 유리

const set = new Set([1, 2, 3])
set.add(3)
set.delete(4)
set.has(5)

객체 참조 집합에 대해 GC 친화성을 원하면 WeakSet 사용

const checked = new WeakSet([todo1, todo2])

직렬화 전략

JSON은 Map/Set을 직접 직렬화하지 않음 replacer와 reviver를 이용해 직렬화 규칙을 커스터마이즈 가능

Map/Set을 직렬화를 위해 객체/배열로 변환

function replacer(key, value) {
  if (value instanceof Map) return Object.fromEntries(value)
  if (value instanceof Set) return Array.from(value)
  return value
}

const payload = { set: new Set([1, 2, 3]), map: new Map([["k", "v"]]) }
const json = JSON.stringify(payload, replacer)

역직렬화 시 객체와 배열을 무조건 Map/Set으로 바꾸면 구분 불가 이슈 발생 타입 태그를 추가해 손실 없이 왕복 가능

function taggedReplacer(key, value) {
  if (value instanceof Map) return { __type: 'Map', value: Object.fromEntries(value) }
  if (value instanceof Set) return { __type: 'Set', value: Array.from(value) }
  return value
}

function taggedReviver(key, value) {
  if (value?.__type === 'Map') return new Map(Object.entries(value.value))
  if (value?.__type === 'Set') return new Set(value.value)
  return value
}

const obj = { set: new Set([1, 2]), map: new Map([["key", "value"]]) }
const str = JSON.stringify(obj, taggedReplacer)
const roundTripped = JSON.parse(str, taggedReviver)

언제 무엇을 쓸까 요약

  • 구조가 고정된 도메인 객체는 객체 사용, 예 title, date 같은 명확한 스키마
const event = { title: 'Conf', date: new Date() }
  • 동적 키-값 저장과 빈번한 추가·삭제는 Map 사용
const events = new Map()
events.set(event.id, event)
events.delete(event.id)
  • 순서가 중요하고 중복 허용 목록은 배열
const list = [1, 2, 3, 2]
  • 중복 없는 집합이고 순서 중요하지 않으면 Set
const uniq = new Set([1, 2, 3])

정리

객체는 구조화된 데이터 모델에, Map은 동적 해시맵 패턴에, Set은 고유 집합에 최적 성능은 실제 시나리오에서 검증하되, 키 추가·삭제와 반복·키타입 유연성 측면에서 Map이 갖는 일관된 장점이 큼 메모리 안전성이 필요한 참조 기반 메타데이터에는 WeakMap과 WeakSet 고려 권장

참고자료