맵과 셋 파트 : https://ko.javascript.info/map-set
맵(Map)은 key, value 형태로 값을 저장하는 자료구조로 객체와 차이점이 있다면 맵의 key에는 다양한 자료형이 올 수 있다.
맵에는 다음과 같은 주요 메서드와 프로퍼티가 있다.
new Map() – 맵 생성
map.set(key, value) – key를 이용해 value를 저장
map.get(key) – key에 해당하는 값을 반환. key가 존재하지 않으면 undefined를 반환한다.
map.has(key) – key가 존재하면 true, 존재하지 않으면 false를 반환
map.delete(key) – key에 해당하는 값을 삭제
map.clear() – 맵 안의 모든 요소를 제거
map.size – 요소의 개수를 반환
예시:
let map = new Map();
map.set('1', 'str1'); // 문자형 키
map.set(1, 'num1'); // 숫자형 키
map.set(true, 'bool1'); // 불린형 키
// 객체는 키를 문자형으로 변환,
// 맵은 키의 타입을 변환시키지 않고 그대로 유지하여 아래의 코드는 출력되는 값이 각각 다르다.
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
맵은 객체와 달리 키를 문자형으로 변환하지 않고 key에 자료형 제약이 없다.
map[key] = 2 와 같은 방식으로 객체처럼 map의 값을 설정할 수 있지만 이 방법은 map을 일반 객체처럼 취급하게 돼서 여러 제약이 생기게 된다. (이렇게 사용하지 말라는 의미다)
map을 사용할 땐 map전용 메서드 set, get 등을 사용해야 한다.
맵의 key로는 객체도 들어올 수 있다.
예시
let john = { name: "John" };
// 고객의 가게 방문 횟수 count
let visitsCountMap = new Map();
// john을 맵의 키로 사용
visitsCountMap.set(john, 123);
alert( visitsCountMap.get(john) ); // 123
객체를 키로 사용할 수 있다는 점은 맵의 가장 중요한 기능 중 하나다!
객체는 객체를 키로 사용할 수 없다.
let john = { name: "John" };
let visitsCountObj = {}; // 객체
visitsCountObj[john] = 123; // 객체(john)를 키로 해서 객체에 값(123)을 저장
// 당연하게도 객체가 객체의 key로 들어갈 일은 없다...^^
// key는 [object object] 라는 이름으로 들어가서
// 원하는 값(123)을 얻으려면 아래와 같이 키가 들어갈 자리에 `[object Object]`를 써줘야한다.
alert( visitsCountObj["[object Object]"] ); // 123
visitsCountObj는 객체이기 때문에 모든 키를 문자형으로 변환시킨다(객체가 그렇다 key를 문자형으로 변환시킨다). 이 과정에서 john은 문자형으로 변환되어 "[object Object]"가 된다.
Tip. 맵이 키를 비교하는 방식
맵은 SameValueZero라 불리는 알고리즘을 사용해 맵의 키와 값이 매칭되는지 여부(본문에는 등가 여부라고 되어있었다)를 확인한다. 이 알고리즘은 일치 연산자 ===와 거의 유사하지만, NaN과 NaN을 같다고 취급하는 것에서 일치 연산자와 차이가 있다.
따라서 맵에선 NaN도 키로 쓸 수 있다.
이 알고리즘은 수정하거나 커스터마이징 하는 것이 불가능하다.
Tip. 체이닝
map.set을 호출할 때마다 맵 자신이 반환된다. 이를 이용하면 map.set을 '체이닝(chaining)'할 수 있다.
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
아래 메서드들은 맵의 각 요소를 반복 작업할 수 있도록 해준다.
map.keys() – 각 요소의 키를 모은 반복 가능한(iterable, 이터러블) 객체를 반환한다.
map.values() – 각 요소의 값을 모은 이터러블 객체를 반환한다.
map.entries() – 요소의 [키, 값]을 한 쌍으로 하는 이터러블 객체를 반환한다. 이 이터러블 객체는 for~of반복문의 기초로(대상으로) 쓰인다.
예시
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 키(vegetable)를 대상으로 순회
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 값(amount)을 대상으로 순회
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// [키, 값] 쌍을 대상으로 순회
for (let entry of recipeMap) { // recipeMap.entries()와 동일
alert(entry); // cucumber,500 ...
}
Tip. 맵은 삽입 순서를 기억한다.
맵은 값이 삽입된 순서대로 순회를 돌고 배열과 유사하게 내장 메서드 forEach도 지원한다.
// 각 (키, 값) 쌍을 대상으로 함수를 실행
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 ...
});
1) 각 요소가 키-값 쌍인 배열이나 2)이터러블 객체를 초기화 용도로 맵에 전달해 새로운 맵을 만들 수 있다.
// 각 요소가 [키, 값] 쌍인 배열
let map = new Map([
['1', 'str1'],
[1, 'num1'],
[true, 'bool1']
]);
alert( map.get('1') ); // str1
평범한 객체를 가지고 맵을 만들고 싶다면 내장 메서드 Object.entries(obj)를 활용해야 한다.
이 메서드는 객체의 키-값 쌍을 요소([key, value])로 가지는 배열을 반환한다.
예시
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John
Object.entries를 사용해 객체 obj를 배열 [ ["name","John"], ["age", 30] ]로 바꾸고, 이 배열을 이용해 새로운 맵을 만들었다.
Object.fromEntries를 사용하면 맵을 객체로 바꿀 수 있다!
이 메서드는 각 요소가 [키, 값] 쌍인 배열을 객체로 바꿔준다.
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// 변환된 prices => { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
자료가 맵에 저장되어있는데, 서드파티 코드(제3의 외부 코드)에서 자료를 객체형태로 넘겨받길 원할 때 이 방법을 사용할 수 있다.
예시
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
let obj = Object.fromEntries(map.entries()); // 맵을 일반 객체로 변환 (*)
// 맵이 객체로 변환됨
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
map.entries()를 호출하면 맵의 [키, 값]을 요소로 가지는 이터러블을 반환하여 Object.fromEntries를 사용하기에 알맞다.
(*)로 표시한 줄을 좀 더 짧게 줄이는 것도 가능하다.
let obj = Object.fromEntries(map); // map에서 .entries()를 생략함
1) Object.fromEntries는 인수로 이터러블 객체를 받기 때문에 짧게 줄인 코드도 이전 코드와 동일하게 동작한다(꼭 배열 형태로 인수를 전달할 필요는 없다.).
2) map에서의 일반적인 반복은 map.entries()를 사용했을 때와 같은 키-값 쌍을 반환한다.
3) 즉, 위의 예시는 map과 동일한 키-값을 가진 일반 객체를 얻게 된다.
셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이다. 셋에 키가 없는 값이 저장된다.
주요 메서드는 다음과 같다.
new Set(iterable) – 셋을 만들고 이터러블 객체를 전달받으면(대개 배열을 전달받음) 그 안의 값을 복사해 셋에 넣어준다.
set.add(value) – 값을 추가하고 셋 자신을 반환한다.
set.delete(value) – 값을 제거한다. 호출 시점에 셋 내에 값이 있어서 제거에 성공하면 true, 아니면 false를 반환한다.
set.has(value) – 셋 내에 값이 존재하면 true, 아니면 false를 반환한다.
set.clear() – 셋을 비운다.
set.size – 셋에 몇 개의 값이 있는지 세준다.
셋 내에 동일한 값(value)이 있다면 set.add(value)를 아무리 호출해도 아무런 반응이 없을 것이다. => 셋 내의 값에 중복이 없는 이유
본문에서 아주 적절한 예시를 들고 있다.
방문자 방명록을 만든다고 가정했을 경우 한 방문자가 여러 번 방문해도 방문자를 중복해서 기록하지 않겠다고 결정 내린 상황입니다. 즉, 한 방문자는 '단 한 번만 기록’되어야 한다.
이때 적합한 자료구조가 바로 셋이다.
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// 어떤 고객(john, mary)은 여러 번 방문할 수 있다.
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// 셋에는 유일무이한 값만 저장된다.
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John, Pete, Mary 순으로 출력된다.
}
배열을 사용하여 방문자 정보를 저장한 후, 중복 값 여부는 arr.find를 이용해 확인할 수도 있지만 arr.find는 배열 내 요소 전체를 뒤져 중복 값을 찾기 때문에, 셋보다 성능 면에서 떨어진다. 셋은 값의 유일무이함을 확인하는데 최적화되어있다.
for~of나 forEach를 사용하면 셋의 값을 대상으로 반복 작업을 수행할 수 있다.
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// forEach를 사용해도 동일하게 동작한다.
set.forEach((value, valueAgain, set) => {
alert(value);
});
forEach에 쓰인 콜백 함수는 세 개의 인수를 받는데, 첫 번째는 값, 두 번째도 같은 값인 valueAgain을 받고 있다. 세 번째는 목표하는 객체(셋)이고요. 동일한 값이 인수에 두 번(value, valueAgain) 등장하고 있다.
이렇게 구현된 이유는 맵과의 호환성 때문이다.
맵의 forEach에 쓰인 콜백이 세 개의 인수를 받을 때를 위해서이고 이렇게 구현해 놓았기 때문에 맵을 셋으로 혹은 셋을 맵으로 교체하기가 쉽다.
셋에도 맵과 마찬가지로 반복 작업을 위한 메서드가 있다.
set.keys() – 셋 내의 모든 값을 포함하는 이터러블 객체를 반환
set.values() – set.keys와 동일한 작업을 한다. 맵과의 호환성을 위해 만들어진 메서드이다.
set.entries() – 셋 내의 각 값을 이용해 만든 [value, value] 배열을 포함하는 이터러블 객체를 반환한다. 맵과의 호환성을 위해 만들어졌다.