[모던 자바스크립트 Deep Dive] - (8) Set과 Map

JIN·2024년 11월 17일
post-thumbnail

Set과 Map

자바스크립트에서는 데이터를 담고 순회하기 위해 다양한 자료구조를 제공합니다. 그중 SetMap은 각각 고유한 특징으로 특정 상황에서 유용하게 활용됩니다. 예를 들어, 게시판에서 게시글을 순회하며 출력할 때는 Map을 사용할 수도 있고, 중복되지 않은 데이터를 관리할 때는 Set을 사용할 수도 있습니다.

그럼 SetMap은 각각 어떤 특징이 있고, 각 자료구조를 어떻게 활용할 수 있을까요?

Set과 Map의 특징

Set 객체는 중복되지 않는 유일한 값들의 집합을 표현하는 자료구조입니다. 배열과 비슷하지만 아래와 같은 차이점이 있습니다.

배열구분Set 객체
동일한 값을 중복하여 포함 할 수 있다.
요소 순서에 의미가 있다.
인덱스로 요소에 접근할 수 있다.

Map 객체는 키와 값의 쌍으로 이루어진 컬렉션 입니다. 객체와 유사하지만 아래와 같은 차이점이 있습니다.

객체구분Map 객체
문자열 또는 심벌 값키로 사용할 수 있는 값객체를 포함한 모든 값
이터러블
Object.keys(obj).length요소 개수 확인map.size

각각의 특징을 한줄로 정리하자면,
Set 객체는 중복된 요소를 포함하지 않으며, 순서와 인덱스가 없는 자료구조 이며 순서에 상관없이 하루 동안 각 사람의 방문을 한번씩만 기록하는 도서관 출입 명부와 같습니다.

Map 객체는 키와 값의 쌍으로 이루어진 반복이 가능한 객체 이며 사전과 같이 단어(키)와 정의(값)을 연결해둔 구조와 유사하며, 단어를 추가하고 검색하는데 효율적인 자료구조 입니다.

Set과 Map 생성

Set

Set 객체는 Set 생성자 함수를 사용해 생성할 수 있으며, 생성 시 이터러블(iterable)을 인수로 전달하면 중복된 값은 제거됩니다.

const set = new Set();
console.log(set); // Set(0) {}

const set1 = new Set([1,2,3,3]);
console.log(set1);  // Set(3) {1,2,3}

const set3 = new Set('hello);
console.log(set2);  // Set(4) {"h", "e", "l", "o}                 

중복이 불가능한 Set

기본적으로 중복을 허용하지 않는 Set 객체의 특성을 이용해 배열 내 중복 요소를 손쉽게 제거할 수 있습니다.

// 배열의 중복 요소 제거
const uniq1 = array => array.filter((v, i, self) => self.indexOf(v) === i);
console.log(uniq1([2,1,2,3,4,3,4])); // [2, 1, 3, 4]

// Set을 통한 배열의 중복 요소 제거
const uniq2 = array => [...new Set(array)];
console.log(uniq2([2,1,2,3,4,3,4])) // [2,1,3,4]

Map

Map 객체는 Map 생성자 함수를 사용해 생성할 수 있으며, 빈 값이나 키와 값의 쌍으로 이루어진 이터러블을 인수로 전달하여 생성이 가능합니다.

const map1 = new Map([['key1', 'value1'], ['key2', 'value2']]);
console.log(map1) // Map(2) {'key1' => 'value1', 'key2' => 'value2'}

const map2 = new Map([1,2]) // TypeError: Iterator value 1 is not an entry object

객체를 생성할때 키와 값의 쌍이 아니라면 오류가 발생하고, 중복된 키는 Map 객체에 요소로 저장되지 않습니다.

Map 객체의 요소 취득

Map 객체에서 특정 요소를 취특하려면 .get() 메서드를 사용할수 있습니다. get메서드의 인수로 키를 전달하면 해당 키를 갖는 값을 반환하고, 요소가 없다면 undefined를 반환하게 됩니다.

const map = new Map();

const lee = { name : 'Lee' }
const kim = { name : 'Kim' }

map.set(lee, 'developer').set(kim, 'designer')

console.log(map.get(lee)); // developer
console.log(map.get('key')); // undefined

이터러블(iterable) : 배열, 문자열 등 반복 가능한 객체

요소 추가

Set

Set 객체에 요소를 추가할 경우에는 .add() 메서드를 사용합니다.

const set = new Set();

set.add(1)
console.log(set) // Set(1) {1}

set.add(2).add(3)
console.log(set) // Set(3) {1,2,3}

set.add(3);
console.log(set) // Set(3) {1,2,3}

add 메서드를 연속으로 사용하여 여러 요소를 추가할 수 있으며, 중복을 허용하지 않기에 중복된 요소를 추가할 경우 에러가 발생하지 않고 무시됩니다.

Set 객체에는 객체나 배열과 같은 자바스크립트의 모든 값을 요소로 추가가 가능합니다.

const set = new Set();

set.add(1).add('a').add(true).add(undefined).add(null).add({}).add([]).add(()=> {})

console.log(set) // Set(8) {1,'a',true, undefined,null, {}, [], () => {}}

Map

Map 객체에 요소를 추가할 경우에는 .set() 메서드를 사용합니다.

const map = new Map();

map.set('key1','value1')
console.log(map) // Map(1) {'key1' => 'value1'}

set 메서드는 새로운 요소가 추가된 Map 객체를 반환하며 Set 객체의 add 메서드와 같이 연속으로 사용하여 여러 요소를 추가할 수 있습니다.

const map = new Map();

map.set('key1','value1').set('key2','value2');

console.log(map) // Map(2) {'key1' => 'value1', 'key2' => 'value2'}

Map 객체에서는 중복된 키를 갖는 요소 추가를 허용하지 않아, 중복된 요소를 추가한다면 에러가 발생하지 않고 무시됩니다.

또한 Map 객체의 키 타입에는 제한이 없기에 객체를 포함한 모든 값들을 키로 사용할 수 있습니다. 이러한 특징이 일반 객체와 가장 큰 차이점 이라 합니다.

const map = new Map();

const lee = { name : 'Lee' }
const kim = { name : 'Kim' }

map.set(lee, 'developer').set(kim, 'designer')

console.log(map) // Map(2) { {name: 'Lee'} => 'developer', {name: 'Kim'} => 'designer'}

요소 삭제

Set 객체와 Map 객체 동일하게 요소를 삭제할 경우에는 .delete() 메서드를 사용하며, detete메서드는 삭제 성공 여부를 나타내는 boolean값을 반환합니다.
Set 객체는 순서에 의미가 없고 인텍스를 갖지 않아 인덱스로 요소에 접근할 수 없기에 삭제하려는 요소 값을 인수로 전달해야 합니다.
Map 객체는 삭제하려는 키 값을 넣어서 삭제가 가능합니다.

Set

const set = new Set([1,2,3]);

set.delete(2)
console.log(set) // Set(2) {1,3}

set.delete(0)
console.log(set) // Set(2) {1,3}

Map

set.delete(1).delete(3) // Uncaught TypeError: set.delete(...).delete is not a function

const lee = { name : 'Lee' }
const kim = { name : 'Kim' }

const map = new Map([[lee, 'developer'], [kim, 'designer']])

map.delete(kim);
console.log(map); // Map(1) { {name: 'Lee'} => 'developer' }

존재하지 않는 요소를 삭제하려고 하면 에러 없이 무시가 되지만, add와 달리 detete 메서드는 성공 여부를 나타내는 boolean값을 반환하기 때문에 연속적으로 호출이 불가능하다는 차이가 있습니다.

만약 Set 객체와 Map 객체에서 모든 요소를 일괄 삭제하려면 .clear() 메서드를 사용이 가능합니다. clear 메서드는 언제나 undefined를 반환하게 됩니다.

Set

const set = new Set([1,2,3]);

set.clear()
console.log(set) // Set(0) {}

Map

const map = new Map([['lee', 'developer'], ['kim', 'designer']])
map.clear()
console.log(map) // Map(0) {}

요소 개수 & 존재 여부 확인

Set 객체와 Map 객체 동일하게 요소의 개수를 확인할때는 .size 프로퍼티를 사용합니다.

Set

const { size } = new Set([1,2,3,3]);
console.log(size); // 3

Map

const map = new Map([['key1', 'value1'], ['key2', 'value2']]);
console.log(Map.size); // 2

특정 요소가 존재하는지 확인하기 위해서는 .has() 메서드를 통해 확인이 가능합니다. has 메서드는 특정 요소의 존재 여부를 boolean값으로 반환합니다.

Set

const set = new Set([1,2,3]);

console.log(set.has(2)); // true
console.log(set.has(4)); // false

Map

const lee = { name : 'Lee' }
const kim = { name : 'kim' }

const map = new Map([[lee, 'developer'], [kim, 'designer']])

cosnole.log(map.has(lee)); // true
console.log(map.has('key'); // false                                 

요소 순회

Set 객체와 Map 객체 모두 요소를 순회하기 위해서는 forEach 메서드를 사용하여 가능합니다. 이는 배열에서 요소를 순회하는 forEach와 유사하며 콜백함수 내부에서 총 3가지 인수를 전달받습니다.

  • 첫 번째 인수 : 현재 순회 중인 요소 값
  • 두 번째 인수: 현재 순회 중인 요소 값
  • 세 번째 인수: 현재 순회 중인 Set 객체 자체 / 현재 순회 중인 Map 객체 자체

첫번째 인수와 두번째 인수가 같은 값인데, 이는 Array의 forEach 메서드와 인터페이스를 통일하기 위해서지 다른 의미는 없다고 설명하고 있습니다.

Set

const set = new Set([1,2]);
set.forEach((i, j, set) => console.log('i :', i, 'j :', j, 'set :', set));

/*
i : 1 j : 1 set : Set(2) {1, 2}
i : 2 j : 2 set : Set(2) {1, 2}
*/

Map

const map = new Map([['one', 1], ['two', 2]])
map.forEach((v, k, map) => console.log(v,k,map));

/*
1 'one' Map(2) {'one' => 1, 'two' => 2}
2 'two' Map(2) {'one' => 1, 'two' => 2}
*/

Set 객체와 Map 객체 모두 이터러블로 for...of 문으로도 순회가 가능하고, 스프레드 문법과 배열 디스트럭처링의 대상이 될 수도 있습니다.

Set

const set = new Set([1,2,3]);

for (const value of set) {
  console.log(value); // 1 2 3
}

console.log([...set]); // [1,2,3]

const [a, ...rest] = set;
console.log(a, rest); // 1, [2,3]

Map

const lee = { name : 'Lee' }
const kim = { name : 'kim' }

const map = new Map([[lee, 'developer'], [kim, 'designer']])

for (const value of map) {
  console.log(value); // [{name: 'Lee'}, 'developer'] [{name: 'Kim'}, 'designer']
}

console.log([...map]); //[{name: 'Lee'}, 'developer'], [{name: 'Kim'}, 'designer']

const [a,b] = map;
console.log(a, b) // [{name: 'Lee'}, 'developer'] [{name: 'Kim'}, 'designer']

Map의 요소 값 반환

Map 객체는 키와 값의 쌍으로 구성된 컬렉션으로, 키, 값, 키-값 쌍을 각각 별도로 순회할 수 있는 메서드를 제공합니다.

.keys(): 모든 키를 포함하는 이터러블 객체 반환
.values(): 모든 값을 포함하는 이터러블 객체 반환
.entries(): [키, 값] 쌍을 포함하는 이터러블 객체 반환 (기본적으로 for...of에서 사용)

const map = new Map([['one', 1], ['two', 2], ['three', 3]]);

// 키 순회
for (const key of map.keys()) {
  console.log(key); // 'one', 'two', 'three'
}

// 값 순회
for (const value of map.values()) {
  console.log(value); // 1, 2, 3
}

// 키-값 쌍 순회
for (const [key, value] of map.entries()) {
  console.log(key, value); // 'one' 1, 'two' 2, 'three' 3
}

// for...of를 통한 키-값 순회 (기본 동작은 entries())
for (const [key, value] of map) {
  console.log(key, value); // 동일한 결과
}

추가로 Map 객체는 요소의 순서에 의미를 갖지 않으며, 순회하는 순서는 요소가 추가된 순서를 따르고 있습니다.

집합 연산 (Set)

Set 객체는 수학적 집합을 구현하기 위한 자료구조로 Set 객체를 통해서 교집합, 합집합, 차집합등의 집합 연산을 쉽게 구현할 수 있습니다.

교집합

집합 A와 집합 B의 공통요소 교집합 A∩B는 아래와 같이 구현이 가능합니다.

Set.prototype.intersection = function (set) {
  const result = new Set();
 
  for (const value of set) {
    // 2개의 set에서 공통되는 요소이면 교집합 대상
    if (this.has(value)) result.add(value)
  }
  return result;
};

const setA = new Set([1,2,3,4]);
const setB = new Set([2,4]);

// setA와 setB의 교집합
console.log(setA.intersection(setB)); // Set(2) {2,4}
// setB와 setA의 교집합
console.log(setB.intersection(setA)); // Set(2) {2,4}

합집합

집합 A와 집합 B의 중복요소가 없는 모든 요소인 합집합 A∪B는 아래와 같이 구현이 가능합니다.

Set.prototype.union = function (set) {
  // this(Set 객체)를 복사
  const result = new Set(this);
 
  for (const value of set) {
    // 2개의 Set객체의 모든 요소로 구성 (중복제외)
   	result.add(value)
  }
  return result;
};

const setA = new Set([1,2,3,4]);
const setB = new Set([2,4]);

// setA와 setB의 합집합
console.log(setA.union(setB)); // Set(4) {1,2,3,4}
// setB와 setA의 합집합
console.log(setB.union(setA)); // Set(2) {2,4,1,3}

차집합

집합 A에서는 존재하지만 집합 B에는 존재하지 않는 요소로 구성된 차집합 A-B는 아래와 같이 구현이 가능합니다.

Set.prototype.difference = function (set) {
  // this(Set 객체)를 복사
  const result = new Set(this);
 
  for (const value of set) {
    // 어느 한쪽에는 존재하지만, 다른쪽에는 존재하지 않는 요소로 구성
   	result.delete(value)
  }
  return result;
};

const setA = new Set([1,2,3,4]);
const setB = new Set([2,4]);

// setA와 setB의 차집합
console.log(setA.difference(setB)); // Set(2) {1,3}
// setB와 setA의 차집합
console.log(setB.difference(setA)); // Set(0) {}

부분집합과 상위집합

집합 A가 집합B에 포함된 경우 A⊆B 집합 A는 집합 B의 부분집합이며, 집합 B는 집합 A의 상위집합입니다.

Set.prototype.isSuperset = function (subset) {
  for (const value of subset) {
    // superset의 모든 요소가 subset의 모든 요소를 포함하는지 확인
   	if (!this.has(value)) return false
  }
  return true;
};

const setA = new Set([1,2,3,4]);
const setB = new Set([2,4]);

// setA가 setB의 상위 집합인지 확인
console.log(setA.isSuperset(setB)); // true
// setB와 setA의 상위 집합인지 확인
console.log(setB.isSuperset(setA)); // false

Set과 Map의 활용

그럼 SetMap은 어떤 상황에서 사용할 수 있을까요?

먼저 Set의 경우 중복이 불가능한 특징을 활용하여 배열에서 중복을 제거하거나, 집합 연산을 할때 유용하게 활용이 가능합니다.

Set을 활용한 중복 방지 검증

아래와 같이 데이터를 추가하기 전에 중복 여부를 확인하는 검증에서 활용이 가능합니다.

const uniqueSet = new Set();
const data = [1, 2, 2, 3, 4];

data.forEach(item => {
  if (!uniqueSet.has(item)) {
    uniqueSet.add(item);
    console.log(`${item}이 추가되었습니다.`);
  }
});
// 1이 추가되었습니다.
// 2이 추가되었습니다.
// 3이 추가되었습니다.
// 4이 추가되었습니다.

Map의 경우 키값을 활용한 데이터를 조회하거나, 빈도를 계산하는데 유용하게 사용할 수 있습니다.

Map을 활용한 키-값 조회

아래 코드와 같이 각각 사용자가 어떤 역할을 하고 있는지 빠르게 조회가 가능합니다.

const users = new Map();

const user1 = { id: 1, name: 'Alice' };
const user2 = { id: 2, name: 'Bob' };

users.set(user1, { role: 'admin' });
users.set(user2, { role: 'editor' });

console.log(users.get(user1)); // { role: 'admin' }
console.log(users.get(user2)); // { role: 'editor' }

Map을 활용한 빈도 계산

또한 배열 내 요소가 몇번 등장했는지 확인하는데도 유용하게 사용이 가능합니다.

const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];

const wordCount = new Map();

words.forEach(word => {
  wordCount.set(word, (wordCount.get(word) || 0) + 1);
});

console.log(wordCount); // Map(3) { 'apple' => 3, 'banana' => 2, 'orange' => 1 }

⭐️ 마무리

마지막으로 Set과 Map의 메서드들을 정리하자면 아래와 같습니다.

구분Set 객체Map 객체
생성new Set()new Map()
요소 추가.add().set()
요소 삭제.delete().delete()
요소 일괄 삭제.clear().clear()
요소 개수.size.size
특정 요소 존재 여부.has().has()
요소 순회.forEach().forEach()

Set은 중복되지 않는 고유한 데이터를 관리할 때 유용하며, Map은 키와 값을 활용하는 경우에 배열과 함께 적절히 활용하면 보다 효율적으로 데이터를 처리할 수 있습니다.

각각의 공통점과 차이점을 이해하고 주요 메서드들을 알아두면 다양한 상황에서 편리하게 활용할 수 있을 것입니다!!


출처: 모던 자바스크립트 Deep Dive 37장 Set과 Map (643p ~ 659p)


profile
MAXIMUM EFFORT 🙃

0개의 댓글