Object와 Array로는 부족할 때 쓰는 두 가지 컬렉션
일반 객체(Object)와 배열(Array)만으로도 자바스크립트에서 웬만한 작업은 다 해결되지만, 모든 상황에 딱 맞게 설계된 건 아닙니다. Map과 Set은 이런 빈틈을 메워 주는 내장 컬렉션으로, 크게 두 가지 용도가 있습니다. 문자열이 아닌 값을 키로 쓰는 조회, 그리고 중복 없는 멤버십 검사입니다.
둘 다 ES2015부터 언어에 포함됐고, 이터러블이며 .size 속성을 가지고, 스프레드 연산자와도 잘 어울립니다. 개념 자체는 단순합니다.
Map— 객체와 비슷하지만, 어떤 값이든 키로 쓸 수 있고 입력 순서도 그대로 유지됩니다.Set— 배열과 비슷하지만, 값이 중복되지 않고 조회가 빠릅니다.
자바스크립트 Map 만들고 사용하기
Map은 키/값 쌍을 저장합니다. new Map()으로 인스턴스를 만든 뒤 .set(), .get(), .has(), .delete()로 다룹니다.
생성자에 [key, value] 쌍의 배열을 넘겨서 초기값을 한 번에 세팅할 수도 있습니다.
이 2요소 배열 형태는 Map을 다룰 때 어디서나 마주치게 됩니다. 순회할 때 엔트리가 표현되는 방식이 바로 이 구조거든요.
Map vs Object: 굳이 Map을 써야 할까?
그냥 객체(Object)로도 같은 일을 할 수 있을 것 같죠. 실제로 대부분의 경우엔 그렇습니다. 하지만 Map은 객체가 가진 몇 가지 불편한 지점을 깔끔하게 해결해 줍니다:
객체는 Object.prototype을 상속받기 때문에 toString, constructor, hasOwnProperty 같은 키가 모든 객체에 이미 들어 있습니다. 반면 Map에는 이런 잡동사니가 없어서, 내가 넣은 키만 존재합니다.
이 외에도 알아둘 만한 차이점은 다음과 같습니다:
- 어떤 타입이든 키로 사용 가능. Map은 객체, 함수, 숫자, 불리언을 모두 키로 받습니다. 객체는 문자열이 아닌 키를 조용히 문자열로 바꿔버리기 때문에
obj[1]과obj["1"]이 같은 자리를 가리킵니다. - 삽입 순서가 보장됨. Map은 값을 넣은 순서대로 순회합니다. 객체도 대체로 그렇지만, 숫자처럼 생긴 문자열 키는 먼저 정렬되어 버리는 미묘한 함정이 있습니다.
- 내장된 size 속성.
map.size는 O(1)입니다. 객체에서는Object.keys(obj).length를 써야 하는데, 이건 매번 배열을 새로 만듭니다. - 잦은 변경에 최적화. 자바스크립트 엔진은 Map을 빈번한 추가/삭제에 맞춰 튜닝해 둡니다. 객체는 형태가 고정된 레코드에 맞춰 최적화되어 있죠.
{ name, email, age }처럼 정해진 문자열 키를 가진 레코드를 표현할 때는 객체를 쓰세요. 반대로 키가 동적이거나, 문자열이 아니거나, 항목을 자주 추가/삭제해야 한다면 Map이 정답입니다.
Map 순회하기
Map은 iterable이기 때문에 for...of를 바로 쓸 수 있고, 각 항목을 구조 분해하는 것도 자연스럽습니다:
키만 뽑고 싶으면 .keys(), 값만 뽑고 싶으면 .values()를 쓰면 됩니다. 취향에 따라 .forEach()도 쓸 수 있고요:
Map을 다시 일반 객체나 배열로 되돌리고 싶다면, 스프레드 연산자를 활용하면 됩니다.
Set 만들기와 활용법
Set은 중복을 허용하지 않는 자료구조입니다. 이미 들어있는 값을 다시 추가해도 아무 일도 일어나지 않습니다:
중복 여부는 기본적으로 === 와 동일한 규칙으로 판단합니다. 단, 한 가지 예외가 있는데요. 일반적으로 NaN === NaN 은 false 이지만, Set 안에서는 NaN 을 서로 같은 값으로 취급합니다.
생성자에 이터러블(iterable)을 넘기면 그 값들로 Set 을 초기화할 수 있습니다. 배열 중복 제거 트릭이 바로 여기서 나옵니다:
원시 타입 한 줄이면 끝입니다. 객체 배열에는 이 방법이 안 통하는데, 필드 값이 같더라도 서로 다른 객체면 별개의 값으로 취급되기 때문이죠. 하지만 문자열, 숫자, 불리언이라면 이게 가장 관용적인 중복 제거 방식입니다.
Set vs Array: 언제 바꿔야 할까
배열과 Set 모두 값을 모아두는 자료구조인데, 어느 쪽을 언제 써야 할까요?
이럴 땐 Set이 정답입니다:
- 값이 반드시 유일해야 하고, 그 제약을 런타임이 알아서 지켜주길 원할 때.
- 포함 여부 검사를 자주 해야 할 때.
set.has(x)는 O(1)이지만array.includes(x)는 O(n)입니다. 반복문 안에서라면 이 차이가 금세 쌓입니다. - 삽입 순서만 유지되면 충분할 때. Set은 삽입 순서대로 순회되지만, 인덱스 접근은 지원하지 않습니다.
반대로 배열을 유지해야 하는 경우는:
- 위치 기반 접근이 필요할 때 —
arr[0], 슬라이싱, 정렬 같은 작업들. - 중복 자체가 의미를 가질 때 — 같은 상품이 두 개 담긴 장바구니 같은 상황.
.map,.filter,.reduce같은 배열 메서드를 적극적으로 쓸 때. Set에는 이런 메서드가 없어서 결국 스프레드로 배열로 풀어줘야 합니다.
성능 차이를 간단히 보여주는 예시입니다:
banned이 배열이었다면 filter 콜백이 돌 때마다 리스트 전체를 훑어야 합니다. Set으로 만들어 두면 조회가 상수 시간에 끝나죠.
Set 순회하기
Map과 똑같습니다. for...of로 바로 돌릴 수 있고, 스프레드 연산자를 쓰면 배열로 변환할 수 있어요:
Set도 Map과 일관성을 맞추려고 .keys(), .values(), .entries() 메서드를 제공합니다. Set에서는 키와 값이 사실상 같은 값이긴 하지만요. 실무에서는 그냥 바로 순회하는 경우가 대부분입니다.
실전 예제: 페이지별 순 방문자 수 세기
Map과 Set을 같이 써보겠습니다. 페이지 경로를 키로, 방문자 ID를 담는 Set을 값으로 가지는 Map입니다:
Map은 경로와 버킷을 매핑하는 역할을, Set은 각 버킷 안에서 중복을 걸러내는 역할을 맡습니다. 물론 일반 객체와 배열로도 똑같이 구현할 수는 있지만, 그러면 indexOf로 일일이 확인하고 hasOwnProperty로 방어 코드를 덕지덕지 붙여야 합니다.
WeakMap과 WeakSet 간단히 살펴보기
특정한 상황에서만 쓰이는 관련 컬렉션으로 WeakMap과 WeakSet이 있습니다. 이 둘은 참조를 약하게(weak) 잡는다는 특징이 있어서, WeakMap의 키나 WeakSet의 값이 다른 곳에서 더 이상 참조되지 않으면 자동으로 가비지 컬렉션 대상이 됩니다.
키로는 오직 객체만 받고, 순회도 안 되며 .size 같은 것도 없습니다. 이건 일부러 그렇게 만든 건데요, 만약 순회가 가능했다면 가비지 컬렉터의 동작이 외부로 드러나 버리기 때문입니다. 이들은 내가 직접 소유하지 않은 객체에 메타데이터를 캐싱할 때 유용하지만, 실제 코드에서는 자주 쓰이지 않습니다.
다음 주제: JSON
Map과 Set은 메모리 안에서는 훌륭하지만, 둘 다 JSON.stringify를 거치고 나면 원형 그대로 살아남지 못합니다. Map은 {}, Set도 {}로 변해 버리죠. 다음 페이지에서는 JSON을 다룹니다. 데이터를 직렬화하고 파싱하는 방법은 물론, 이번 페이지에서 소개한 컬렉션들을 네트워크나 파일 경계 너머로 보내야 할 때 쓸 수 있는 패턴들까지 함께 살펴봅니다.
자주 묻는 질문
Map이랑 일반 객체(Object)는 뭐가 다른가요?
Map은 객체, 함수, 숫자 등 어떤 값이든 키로 쓸 수 있지만, 일반 객체는 키를 문자열(또는 심볼)로 강제 변환합니다. 또 Map은 .size로 크기를 바로 알 수 있고, 삽입 순서대로 순회되며, 프로토타입에서 키를 상속받지 않아서 toString이나 constructor 같은 이름과 충돌할 걱정이 없습니다. 키가 문자열이 아니거나 항목을 자주 추가·삭제해야 한다면 Map이 정답입니다.
Set은 언제 쓰나요?
Set은 중복을 허용하지 않고 고유한 값만 저장합니다. 배열 중복 제거할 때 제일 간단한 방법이 바로 [...new Set(arr)]이에요. 그리고 .has()가 O(1)로 동작하기 때문에, 반복문 안에서 포함 여부를 체크할 때는 array.includes()보다 훨씬 빠릅니다.
Map은 어떻게 순회하나요?
for...of를 바로 쓸 수 있습니다. for (const [key, value] of myMap)처럼 구조 분해로 각 항목을 꺼내면 돼요. 필요에 따라 myMap.keys(), myMap.values(), myMap.entries()를 돌려도 됩니다. 순회 순서는 삽입 순서가 그대로 보장되는데, 일반 객체는 숫자처럼 생긴 키가 섞이면 순서가 보장되지 않을 수 있다는 점에서 차이가 큽니다.