notes
37장. Set과 Map
모던 JS Deep Dive — Set·Map의 중복 제거·키-값 저장과 순회 핵심 정리.
참고: 이웅모 《모던 자바스크립트 Deep Dive》 37장 · 본인 말로 정리 (원문 긴 인용·캡처 없음)
보강: MDN Set / Map / WeakMap, Keyed collections
알고 쓰니? 😅
배열 중복 제거할 때 [...new Set(arr)] 한 줄은 자주 썼다.
Map은… 솔직히 new Map() 정도는 쳐 봤는데, 객체랑 뭐가 다른지 말로 설명하라면 막혔다.
WeakMap은 이름만 알고 “약한가 보다” 하고 넘어갔다.
Deep Dive 37장 읽으면서 질문이 쌓였다.
Set은 배열 아니야? 왜 인덱스가 없어?
Map 쓰면 객체보다 뭐가 좋아?
NaN을 Set에 두 번 넣으면 몇 개야?
그래서 MDN keyed collections이랑 SameValueZero 쪽을 같이 찾아보며 내 말로 다시 정리했다.
Set — “같은 건 한 번만” 바구니
이게 무슨 말이냐
배열은 중복 OK, 순서 OK, arr[0] OK.
Set은 유일 값 가방이다.
같은 값을 또 넣으면 무시되고, 인덱스 접근은 없다.
수학 집합이랑 비슷하다고 보면 감이 온다.
const s = new Set([1, 2, 2, 3])
console.log(s.size) // 3 — length 아님!
s.add(4).add(4)
console.log([...s]) // [1,2,3,4]
처음엔 s.length 찾다가 멍해졌다.
찾아보니 개수는 size다.
배열 습관을 버리는 게 첫 숙제.
SameValueZero — NaN이 왜 같지?
console.log(NaN === NaN) // false
const s = new Set([NaN, NaN, 0, -0])
console.log(s.size) // 2
console.log(s.has(NaN)) // true
이게 무슨 말이냐.
평소 ===로는 NaN끼리 다른데, Set/Map은 SameValueZero라서 NaN을 같은 값으로 본다.
+0이랑 -0도 같다.
정리하면: “비교 연산자 감각”이랑 “컬렉션 멤버십 감각”은 따로 외워야 한다.
indexOf(NaN)은 -1인데 includes(NaN)은 true인 것도 같은 계열 함정.
객체는 내용이 같아도 다른 사람
const a = { id: 1 }
const b = { id: 1 }
console.log(new Set([a, b]).size) // 2
비유하면 비슷해 보이는 명함 두 장이지, 같은 사람이 아니다.
서버에서 매번 새 객체로 오는 “같은 id”를 Set에 넣으며 중복이 사라지길 기대하면 실패한다.
그때는 id 문자열을 Set에 넣거나, Map으로 id → 객체 정규화.
집합 연산 — 손으로 먼저, 네이티브는 보너스
Deep Dive 37.1.8절 방식으로 합·교·차를 짜 보면 “Set + 이터러블” 조합이 몸에 붙는다.
const A = new Set([1, 2, 3])
const B = new Set([2, 3, 4])
const union = new Set([...A, ...B])
const inter = new Set([...A].filter((x) => B.has(x)))
const diff = new Set([...A].filter((x) => !B.has(x)))
MDN 보니 요즘 환경엔 union / intersection / difference 같은 메서드도 있다.
인자가 꼭 Set일 필요 없고 set-like(size, has, keys)면 Map도 들어간다.
지원 범위 확인 전엔 위 패턴이 안전하다.
Map — 객체 키를 진짜 키로 쓰고 싶을 때
객체랑 뭐가 다르냐
객체 키는 문자열·심볼로 강제된다.
'constructor' 같은 이름이 오면 무섭고, 개수는 Object.keys로 세야 하고, 삽입 순도 규칙이 미묘하다.
Map은 아무 값이나 키, size 있음, 삽입 순 순회 믿어도 됨.
MDN도 삽입 순을 기억한다고 못 박는다.
const user = { id: 1 }
const meta = new Map()
meta.set(user, { role: 'admin' })
meta.set('version', 3)
console.log(meta.get(user), meta.size)
실수 일기
meta[user] = ...로 넣음 → Map 엔트리가 아니라 객체 구멍.size에 안 잡힘. 반드시set/get.{ id: 1 }을 새로 만들어get→ 참조가 달라 miss.JSON.stringify(map)→ 빈 객체처럼 죽음.Object.fromEntries(map)필요.
이게 무슨 말이냐 하면, Map은 “특별한 객체”가 아니라 전용 API로만 다루는 컬렉션이다.
브라켓 문법 유혹은 버려야 한다.
WeakMap — 왜 size가 없어?
찾아보니 키가 객체(또는 non-registered symbol)이고, 키 참조가 약함.
키 객체가 别处에서 안 쓰이면 엔트리도 GC 후보.
열거·size가 없는 이유: 키 목록을 보여주는 순간 GC 타이밍이 관측되어 비결정적이 된다.
MDN이 강조하는 지점이다.
비유: Map은 “명부철”이라 명단을 다 펼 수 있다.
WeakMap은 “포스트잇을 객체에 살짝 붙인 것”이라, 객체가 사라지면 메모도 같이 사라지길 원하고, 명단 출력은 안 한다.
언제 뭐 쓰지? (고민 결과 표)
더 헷갈렸던 것들 — 질문하고 내가 답하기
Q. Set을 for...in 돌리면?
A. 안 된다. 유사 배열이 아니다. for...of나 forEach.
Q. Map 키로 함수 넣어도 돼?
A. 된다. 참조가 같아야 get 성공. 인라인 화살표를 키로 넣으면 다시 못 찾는다.
Q. Set이 배열보다 항상 빨라?
A. “항상”은 아니다. 다만 멤버십·중복 정책이 API에 드러나서 의도가 빨라진다. 마이크로벤치만 믿지 말고 읽기 쉬운 쪽을 고른다.
Q. 언제 객체로 충분한데 Map을 쓰냐?
A. 처음엔 Map이 멋져 보여서 남용했다. 찾아보니 문자열 키 설정, JSON 왕복, 구조 분해가 중요한 곳은 객체가 가볍다. Map은 “키가 객체거나, size/순회 계약이 비즈니스”일 때.
코드로 체화 — LRU 비슷한 캐시
Deep Dive 37장을 덮고 “그래서 실무에 뭐 하냐”가 궁금해서 Map 삽입 순을 이용한 작은 캐시를 짜 봤다.
function makeLru(limit = 50) {
const store = new Map()
return {
get(key) {
if (!store.has(key)) return undefined
const v = store.get(key)
store.delete(key)
store.set(key, v) // 맨 뒤로 = 최근 사용
return v
},
set(key, value) {
if (store.has(key)) store.delete(key)
store.set(key, value)
if (store.size > limit) {
const oldest = store.keys().next().value
store.delete(oldest)
}
},
}
}
이게 무슨 말이냐.
Map은 먼저 넣은 키가 keys()에서 먼저 나온다.
그래서 “가장 오래된 것”을 지울 수 있다.
객체로는 이 순서를 믿기 어렵다.
여기서 깨달은 점: Set/Map을 “신기한 문법”으로 외우지 말고, 순서·유일성·키 타입이라는 세 축으로 고르면 된다.
Deep Dive 37장 + MDN을 덮으며
사람들은 이런 식으로 자료를 고르더라.
중복이 싫으면 Set, 키가 자유로워야 하면 Map, 키 수명이면 WeakMap.
나는 앞으로 new Set(arr)만 복붙하지 않고, “왜 Set인지” 한 줄 주석을 남기기로 했다.
한 줄로 나에게
Set = 유일 값 가방 (SameValueZero).
Map = 자유 키 + size + 삽입 순.
WeakMap = 열거는 포기하고 키 수명을 존중.
다음엔 Deep Dive 38장 — 브라우저가 화면을 그리는 파이프라인으로 간다.