개요
객체는 만능처럼 보이지만 모든 키-값 저장에 적합한 도구는 아님 키를 자주 추가·삭제하고 임의 키로 조회하는 해시맵 유사 패턴에서는 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]] = myMapLRU 캐시 같은 순서 기반 전략 구현에 유리함
복사
객체는 전개나 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 고려 권장
참고자료
- https://www.builder.io/blog/maps
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
- https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
- https://mrale.ph/blog/2012/12/15/microbenchmarks-fairy-tale.html
- https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
- https://www.zhenghao.io/posts/object-vs-map#performance-extravaganza