배열에서 중복 값을 제거하거나, 객체처럼 키-값을 빠르게 찾아야 할 때 대부분의 개발자들은 for 루프를 돌리거나 filter() 메서드를 중첩해서 사용한다. 다만 이런 방식들은 느리고 코드도 복잡해진다. 자바스크립트의 Map과 Set은 이런 상황에서 훨씬 효율적이고 명확한 해결책을 제공하는데, 많은 개발자들이 여전히 배열 메서드만 고집한다. 이번엔 Map과 Set이 정확히 뭔지, 언제 써야 하는지, 그리고 실무에서 어떻게 활용하는지 완벽하게 정리해서 소개하겠다.

 

1단계. Map과 Set의 기초 개념 이해하기

Set은 중복을 허용하지 않는 자료구조다. 배열과 달리 같은 값을 여러 번 추가해도 하나만 저장된다. Map은 키-값 쌍을 저장하는데, 객체와 달리 키로 문자열뿐 아니라 객체나 함수도 쓸 수 있다. 그리고 Map의 크기를 언제든 .size로 바로 확인할 수 있다.

배열의 경우 중복 제거를 위해 includes()나 indexOf()를 반복해야 하고, 객체는 키가 항상 문자열이어야 한다. Map과 Set은 이런 제약 없이 훨씬 빠르고 직관적이다.

 

2단계. Set으로 배열 중복 제거하기

Set의 가장 간단한 사용법은 배열 중복 제거다. 배열을 Set으로 변환한 뒤 다시 배열로 돌리면 된다.

✗ 잘못된 방법 (느림):
const arr = [1, 2, 2, 3, 3, 3, 4];
const result = arr.filter((value, index) => arr.indexOf(value) === index);
console.log(result); // [1, 2, 3, 4]
// 문제: 배열 길이만큼 반복하면서 indexOf()가 매번 전체 배열을 다시 탐색한다. O(n²) 시간 복잡도.
✓ 올바른 방법 (빠름):
const arr = [1, 2, 2, 3, 3, 3, 4];
const result = [...new Set(arr)];
console.log(result); // [1, 2, 3, 4]
// 이유: Set은 내부적으로 해시 테이블을 사용해 O(1)에 중복 확인. 전체 O(n) 시간 복잡도.

객체 배열에서도 Set을 쓸 수 있다. 다만 객체는 참조 비교가 되므로, 내용이 같아도 다른 객체로 간주된다.

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice' } // 내용은 같지만 다른 객체
];

const uniqueUsers = [...new Set(users)];
console.log(uniqueUsers.length); // 3 (내용 기반 중복 제거가 안 됨)

// 올바른 방법: id 기준으로 중복 제거
const uniqueById = [...new Map(users.map(u => [u.id, u])).values()];
console.log(uniqueById.length); // 2
console.log(uniqueById); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

 

3단계. Map으로 키-값 저장 및 빠른 조회하기

객체로 데이터를 저장할 때는 for...in 루프가 프로토타입 체인을 따라가거나, 숫자 키의 자동 정렬이 발생한다. Map은 이런 부작용이 없고, 데이터 개수도 .size로 바로 알 수 있다.

✗ 잘못된 방법 (객체 사용):
const cache = {};
cache[1] = 'value1';
cache['1'] = 'value1 overwrite'; // 숫자 1과 문자 '1'이 같은 키로 간주됨
cache[{ id: 1 }] = 'object'; // 객체가 [object Object] 문자열로 변환됨

console.log(Object.keys(cache)); // ['1', '[object Object]']
console.log(cache.length); // undefined (객체는 length 속성이 없음)
✓ 올바른 방법 (Map 사용):
const cache = new Map();
cache.set(1, 'value1');
cache.set('1', 'value1-string');
cache.set({ id: 1 }, 'object-value');

console.log(cache.get(1)); // 'value1'
console.log(cache.get('1')); // 'value1-string'
console.log(cache.get({ id: 1 })); // undefined (다른 객체이므로 찾지 못함)

const key = { id: 1 };
cache.set(key, 'object-value');
console.log(cache.get(key)); // 'object-value' (같은 참조이면 찾음)

console.log(cache.size); // 4

 

4단계. 실전 예제: API 응답 캐싱과 사용자 추적

API 응답을 캐싱할 때 Map을 쓰면 요청 URL을 키로, 응답 데이터를 값으로 저장할 수 있다.

const apiCache = new Map();
const MAX_CACHE_SIZE = 100;

async function fetchWithCache(url) {
  // 이미 캐시에 있으면 바로 반환
  if (apiCache.has(url)) {
    console.log(`캐시에서 ${url} 로드됨`);
    return apiCache.get(url);
  }

  // 캐시가 가득 차면 가장 오래된 항목 제거 (FIFO)
  if (apiCache.size >= MAX_CACHE_SIZE) {
    const oldestKey = apiCache.keys().next().value;
    apiCache.delete(oldestKey);
  }

  // API 요청
  console.log(`API 호출: ${url}`);
  const response = await fetch(url);
  const data = await response.json();

  // 캐시에 저장
  apiCache.set(url, data);
  return data;
}

// 사용 예
await fetchWithCache('https://api.example.com/users/1'); // API 호출
await fetchWithCache('https://api.example.com/users/1'); // 캐시에서 로드

Set을 활용한 방문자 추적도 간단하다.

class VisitorTracker {
  constructor() {
    this.uniqueVisitors = new Set();
    this.pageViews = new Map();
  }

  track(visitorId, pageUrl) {
    // 유니크 방문자 저장
    this.uniqueVisitors.add(visitorId);

    // 페이지별 조회수 증가
    if (this.pageViews.has(pageUrl)) {
      this.pageViews.set(pageUrl, this.pageViews.get(pageUrl) + 1);
    } else {
      this.pageViews.set(pageUrl, 1);
    }
  }

  getStats() {
    return {
      totalUniqueVisitors: this.uniqueVisitors.size,
      pageViews: Object.fromEntries(this.pageViews),
      mostVisitedPage: [...this.pageViews.entries()].reduce(
        (max, [page, count]) => (count > max[1] ? [page, count] : max),
        ['', 0]
      )[0]
    };
  }
}

const tracker = new VisitorTracker();
tracker.track('user123', '/home');
tracker.track('user456', '/home');
tracker.track('user123', '/about');
tracker.track('user789', '/home');

console.log(tracker.getStats());
// { totalUniqueVisitors: 3, pageViews: { '/home': 3, '/about': 1 }, mostVisitedPage: '/home' }

 

5단계. 주의사항과 흔한 실수
실수문제올바른 방법
new Set(배열).lengthSet은 length 속성이 없음new Set(배열).size
set1 === set2참조 비교라 항상 false[...set1].every(item => set2.has(item))
map.get(객체)다른 객체는 찾지 못함같은 참조를 유지하거나 문자열 키 사용
new Set([객체1, 객체2])내용 기반 중복 제거 안 됨Map + 고유 키 조합 사용

 

Map과 Set은 배열과 객체의 한계를 극복하는 현대적인 자료구조다. 중복 제거에는 Set, 빠른 조회에는 Map이라는 원칙을 기억하자. 이 글의 캐싱 예제를 참고해 현재 프로젝트의 배열 반복문을 Map이나 Set으로 바꿔보면, 코드도 더 명확해지고 성능도 눈에 띄게 개선될 것이다.