개요

객체는 만능처럼 보이지만 모든 키-값 저장에 적합한 도구는 아님 키를 자주 추가·삭제하고 임의 키로 조회하는 해시맵 유사 패턴에서는 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 고려 권장

참고자료