JavaScript Map·Set: 값 조회와 중복 제거 실습

2026.09.05·수정 2026.09.13·약 11분·작성: 해비·블로그 소개

Map은 이름표로 찾고, Set은 중복 없이 모읍니다

상품 이름으로 가격을 찾는 작은 예제부터 시작합니다. Map의 저장·조회 방법을 익힌 다음 과일 개수를 세어 보고, Set으로 같은 과일을 한 번만 남겨 보겠습니다.

변수와 배열을 알고 있으면 시작할 수 있습니다. new Map()new Set()은 각각 빈 Map과 빈 Set을 만드는 표현입니다. 지금은 class 문법이나 내부 구현을 알 필요가 없습니다. 아래 예제는 각각 독립적으로 브라우저 개발자 도구의 Console에 붙여 넣어 실행할 수 있습니다. 변수 이름을 다시 선언했다는 오류가 나오면 새로고침 후 해당 예제를 실행하세요.

1. Map을 가격표라고 생각해 봅니다

가게에서 사과 가격을 찾는다고 생각해 보세요. 상품 이름과 가격을 한 쌍으로 적어 두면 “사과”라는 이름표로 가격을 꺼낼 수 있습니다. 이 이름표를 키(key), 그 이름표에 저장한 내용을 값(value)이라고 합니다.

키: 찾을 때 쓰는 이름표 값: 저장한 내용
사과 1000
바나나 1500

여기서 키는 과일 이름, 값은 가격입니다. 배열의 0번·1번처럼 위치 번호로 찾는 대신, 이름표를 사용한다는 점에 주목하세요.

const prices = new Map();
prices.set('사과', 1000);
prices.set('바나나', 1500);
console.log(prices.get('사과'));
console.log(prices.get('바나나'));

출력:

1000
1500

첫 줄에서 빈 가격표를 만듭니다. set('사과', 1000)은 사과라는 키에 1000을 저장합니다. get('사과')는 같은 키에 저장한 값을 읽습니다. set은 쓰기, get은 읽기로 기억해 보세요.

목록을 차례로 검색하는 방식과 이름표로 값을 조회하는 방식 비교

2. 같은 키에 다시 저장하면 어떻게 될까요?

사과 가격이 1200으로 올랐습니다. 같은 키에 다시 set하면 가격이 바뀝니다. 사과 항목이 두 개 생기는 것은 아닙니다. 한 키는 한 값을 가리킵니다.

const prices = new Map();
prices.set('사과', 1000);
prices.set('사과', 1200);
console.log(prices.get('사과'));
console.log(prices.size);
console.log(prices.has('포도'));
console.log(prices.get('포도'));

출력:

1200
1
false
undefined

size는 저장한 키의 개수입니다. 사과 하나만 있으므로 1입니다. has('포도')는 “포도라는 키가 있는가?”를 물어봅니다. 없으므로 false가 나옵니다. 없는 키를 get으로 읽으면 값이 없다는 뜻의 undefined가 나옵니다.

배열 개수는 length, Map 개수는 size라는 이름을 사용합니다. 지금은 필요한 순간에 이 글을 다시 보고 써도 괜찮습니다.

잠깐 확인: 사과에 1000을 저장하고, 바나나에 1500을 저장하고, 사과에 1200을 저장했습니다. 키는 몇 개이고 사과의 값은 얼마일까요?

정답과 이유 보기

키는 2개, 사과의 값은 1200입니다. 마지막 저장은 새 상품을 추가한 것이 아니라 기존 사과의 값을 바꾼 것입니다.

3. 가격 대신 등장 횟수를 저장해 봅니다

이번에는 장바구니에 ['사과', '바나나', '사과']가 있습니다. “사과가 몇 개 있나요?”에 답하고 싶습니다. Map의 키는 과일 이름으로 그대로 두고, 값에 가격 대신 지금까지 센 개수를 넣으면 됩니다.

읽은 과일 읽기 전 개수 이번 과일까지 센 뒤 Map
첫 사과 없음 → 0으로 시작 사과: 1
바나나 없음 → 0으로 시작 사과: 1, 바나나: 1
두 번째 사과 사과가 이미 1개 사과: 2, 바나나: 1

새 과일을 읽을 때마다 하는 일은 같습니다. 이전 개수를 읽고 → 1을 더하고 → 같은 키에 다시 저장합니다. 사과가 두 번째로 나왔을 때 1을 새로 저장하면 안 됩니다. 기존 1개를 잊지 않고 2개로 바꾸어야 합니다.

const counts = new Map();
counts.set('사과', 1);
const previous = counts.get('사과');
counts.set('사과', previous + 1);
console.log(counts.get('사과'));

출력:

2

previous에는 이전 개수인 1이 들어갑니다. 다음 줄에서 previous + 1, 즉 2를 사과 키에 다시 저장합니다. Map이 알아서 개수를 세는 것이 아닙니다. 우리가 “읽고 1을 더해 다시 저장하는 코드”를 작성한 것입니다.

4. 처음 나온 과일부터 처리하는 전체 코드

처음 나온 과일은 아직 저장된 개수가 없습니다. 따라서 없는 과일은 0부터 시작하도록 처리합니다. for...of는 배열에서 과일을 하나씩 꺼내 같은 일을 반복합니다.

const fruits = ['사과', '바나나', '사과'];
const counts = new Map();
for (const fruit of fruits) {
  if (counts.has(fruit) === false) {
    counts.set(fruit, 0);
  }
  const previous = counts.get(fruit);
  counts.set(fruit, previous + 1);
}
console.log(counts.get('사과'));
console.log(counts.get('바나나'));
console.log(counts.size);

출력:

2
1
2

counts.has(fruit) === false는 아직 세어 본 적이 없는 과일인지 확인합니다. 처음이라면 0을 저장합니다. 이미 센 과일은 이 부분을 건너뛰므로 기존 개수가 사라지지 않습니다. 그다음 get으로 개수를 읽어 1을 더합니다.

마지막 size는 과일 총개수가 아니라 과일 종류의 수입니다. 장바구니에는 3개가 있지만, Map의 키는 사과와 바나나 두 개입니다. 이 차이를 이해하면 투표수, 방문 횟수, 단어 등장 횟수에도 같은 방식을 적용할 수 있습니다.

직접 해 보기: fruits를 ['사과', '사과', '바나나', '포도', '바나나']로 바꾸면 사과 개수, 바나나 개수, size는 각각 얼마일까요?

정답과 과정 보기

2, 2, 3입니다. Map에는 사과: 2, 바나나: 2, 포도: 1이 남습니다. 전체 과일 5개와 종류 3개는 다른 숫자입니다. 사과가 나오는 첫 번째와 두 번째 반복을 종이에 적어 보세요.

5. 횟수가 필요 없으면 Set으로 충분합니다

이번에는 “무슨 과일이 들어 있나요?”만 알고 싶습니다. 사과가 몇 개인지는 필요 없습니다. 같은 값을 한 번만 보관하는 Set을 사용할 수 있습니다. Map과 달리 키와 값을 따로 주지 않습니다. 보관할 값 하나만 add로 넣습니다.

const kinds = new Set();
kinds.add('사과');
kinds.add('바나나');
kinds.add('사과');
console.log(kinds.size);
console.log(kinds.has('사과'));
console.log(kinds.has('포도'));

출력:

2
true
false

세 번 add했지만 사과가 겹치므로 두 종류만 남습니다. 같은 값을 다시 넣어도 개수가 늘지 않습니다. has는 Map에서와 마찬가지로 들어 있는지 확인합니다. Set에는 개수를 따로 저장하지 않았으므로, 이 Set만 보고 원래 사과가 두 개였다는 사실은 알 수 없습니다.

하려는 질문 선택 저장하는 내용
사과 가격이 얼마인가요? Map 사과 → 1000
사과가 몇 번 나왔나요? Map 사과 → 2
사과가 들어 있나요? Set 사과라는 값 자체

6. 중복을 없앤 배열이 필요하다면

결과를 다시 배열로 사용하고 싶을 때가 있습니다. 먼저 배열로 Set을 만들고, 그 Set을 Array.from으로 배열로 바꾸면 됩니다. 두 단계를 나누어 읽어 보세요.

const fruits = ['사과', '바나나', '사과'];
const kinds = new Set(fruits);
const uniqueFruits = Array.from(kinds);
console.log(uniqueFruits.join(', '));
console.log(fruits.length);

출력:

사과, 바나나
3

new Set(fruits)는 배열의 값을 받아 중복 없이 모읍니다. Array.from(kinds)는 그 값을 새 배열에 담습니다. 원본 fruits는 수정하지 않았으므로 길이가 여전히 3입니다. 문자열이나 숫자는 처음 등장한 순서로 남으며, 자동으로 가나다순이나 숫자순으로 정렬되는 것은 아닙니다.

7. 연습 문제에 적용합니다

코딩테스트: 학급 회장 득표수 세기에서는 과일 이름 대신 후보 이름을 키로 두면 됩니다. 투표 하나를 읽을 때마다 그 후보의 득표수를 1 올립니다. 먼저 후보별 표를 모두 센 다음, 가장 큰 득표수를 찾는 단계로 나누어 보세요. 해설을 보기 전에 “이 문제에서 키와 값은 무엇인가?”부터 적어 보면 좋습니다.

이 글에서는 Map·Set을 사용하는 법을 배웠습니다. 내부에서 어떻게 빠르게 찾는지 직접 구현하는 해시 테이블은 중급에서 다룹니다. 지금 당장 해시 함수나 복잡도 표까지 외울 필요는 없습니다.

배열·스택·큐와 언제 구분해서 쓰는지 헷갈린다면 자료구조 선택 가이드의 쉬운 상황별 비교로 돌아가 보세요.

더 알아보기: 객체를 넣을 때는 주의하세요

이 글에서는 문자열 과일 이름을 사용했습니다. 객체는 내용이 같다고 자동으로 하나가 되지 않습니다. 예를 들어 따로 만든 { id: 1 } 객체 두 개는 Set에 두 개로 들어갑니다. 객체가 같은 것을 가리키는지로 구분하기 때문입니다. 상품 객체의 id가 같으면 하나로 처리하고 싶다면 객체 자체 대신 id를 Set에 저장하는 식으로 기준을 정하세요.

객체는 내용이 같아도 서로 다른 객체이면 별도로 구분됨

일반적인 Map에는 undefined도 값으로 저장할 수 있습니다. 따라서 키가 존재하는지 자체를 알고 싶으면 get 결과 대신 has로 확인하는 편이 분명합니다. 위 개수 세기 예제는 숫자만 저장하므로 이 경우를 걱정하지 않아도 됩니다.

더 알아보기: Map이 항상 더 좋은가요?

작은 배열에서 값을 한 번 찾는 일이라면 배열로 충분할 수 있습니다. 여러 번 같은 이름표로 찾을 때는 Map을 만들어 재사용하는 것이 유용합니다. 다만 Map을 만드는 데도 시간과 추가 공간이 듭니다. JavaScript 규격은 Map을 반드시 해시 테이블로 구현하라고 정하지 않습니다. 지금은 “번호로 찾기, 이름표로 찾기, 중복 없이 모으기”의 용도를 구분하는 데 집중하세요.

참고 자료

직접 실습: 연산 비용으로 구조를 설명합니다

실습 주제: JavaScript Map·Set: 값 조회와 중복 제거 실습

  1. 본문 구현에서 저장되는 값과 연결 관계를 그림으로 적습니다.
  2. 조회·삽입·삭제 중 이 구조가 가장 자주 수행할 연산을 고릅니다.
  3. 연산 전후에도 유지되어야 하는 규칙을 한 문장으로 적습니다.
  4. 배열이나 Map 같은 다른 구조로 바꿨을 때 시간·공간 비용을 비교합니다.
풀이 기준과 확인 결과

메서드 이름만 외우지 말고 한 번의 연산에서 어떤 값과 연결이 바뀌는지 추적하세요. 빈 구조, 원소 한 개, 중복값, 연속 삽입·삭제를 실행했을 때 본문이 설명한 불변식이 유지되면 성공입니다.

테스트 체크리스트

  • 빈 구조에 대한 조회·삭제 처리
  • 첫 원소와 마지막 원소 변경
  • 중복값 또는 동일 우선순위 처리
  • 입력 크기가 커졌을 때 예상 복잡도 유지

이 글이 도움이 되었나요?

조회 중

자료구조 학습 순서

필수 18개 · 전체 18개

읽음 기록 관리

전체 과정 목차 (18개)
  1. 필수 학습 · 자료구조 선택 가이드: 연산 비용으로 배열·스택·큐·Set 고르기
  2. 필수 학습 · JavaScript 배열: 인덱스 조회와 삽입·삭제 비용
  3. 필수 학습 · JavaScript Map·Set: 값 조회와 중복 제거 실습 현재 글
  4. 필수 학습 · 자료구조 스택 쉽게 이해하기: push pop으로 문제 풀이 감 잡기
  5. 필수 학습 · 큐와 FIFO: head 인덱스로 JavaScript 대기열 만들기
  6. 필수 학습 · 단방향 연결 리스트: head·tail 삽입과 삭제
  7. 필수 학습 · JavaScript 원형 덱 구현: 양끝 삽입·삭제와 고정 용량 버퍼
  8. 필수 학습 · JavaScript 문자열 해시 테이블 구현: 충돌 처리와 리사이즈, NFC 정규화
  9. 필수 학습 · 트리 자료구조 차이: 이진 트리 BST MST 구분하기
  10. 필수 학습 · JavaScript 이진 탐색 트리 구현: 중복 키와 세 가지 삭제 처리
  11. 필수 학습 · JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수
  12. 필수 학습 · JavaScript 그래프 구현: 인접 리스트·인접 행렬 비교와 BFS
  13. 필수 학습 · JavaScript Union-Find: 경로 압축과 크기 합치기로 연결 상태 관리하기
  14. 필수 학습 · JavaScript Trie: Unicode 접두사 검색과 안전한 삭제 구현
  15. 필수 학습 · JavaScript Fenwick Tree: lowbit로 구간 합과 단일 증가 갱신 구현
  16. 필수 학습 · JavaScript 반복형 세그먼트 트리: 구간 합·단일 대입·결합 순서
  17. 필수 학습 · JavaScript LRU 캐시: Map과 이중 연결 리스트의 불변식
  18. 필수 학습 · JavaScript AVL 트리: 높이 불변식과 LL·RR·LR·RL 삽입 회전

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기