Language/JavaScript

자료형 Map, Set

언젠간코딩잘함 2023. 4. 12. 18:02

JavaScript는 객체와 배열이라는 강력한 자료구조를 제공한다.

 

하지만 현실 세계를 반영하기엔 이 두 자료구조 만으론 부족해서 맵(Map)과 셋(Set)이 등장하게 되었다.

 


Map

 

키가 있는 데이터를 저장한다는 점에서 객체와 유사하다.

다만, 맵은 키에 다양한 자료형을 허용한다는 점에서 차이가 있다.

 

(객체의 key는 항상 스트링형태로 저장)

let map = new Map([
  // 2차원 key, value 형태의 배열
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);
// map 자료형 : {"a" => 1, "b" => 2, "c" => 3}

 

new Map() 맵을 만듬
map.set(key, value) key를 이용해 value를 저장
map.get(key) key에 해당하는 값을 반환. key가 없으면 undefined를 반환
map.has(key) key가 존재하면 ture, 없으면 false를 반환
map.delete(key) key에 해당하는 값을 삭제
map.clear() 맵 안의 모든 요소를 제거
map.size 요소의 개수를 반환
let realMap = new Map();

realMap.set('1', '진솔');   // 문자형 키
realMap.set(1, 'zzinsole');     // 숫자형 키
realMap.set(true, 'zzinsoleKim'); // 불린형 키

/* 2차원 배열 형태로 한방에 선언 할 수 있다
let realMap = new Map([
   ['1', '진솔'],
   [1, 'zzinsole'],
   [true, 'zzonsoleKim']
])*/

// 객체는 키를 문자형으로 변환한다
// 맵은 키의 타입을 변환시키지 않고 그대로 유지, 따라서 아래의 코드는 출력되는 값이 다르다
console.log( realMap.get(1)   ); // 'zzinsole'
console.log( realMap.get('1') ); // '진솔'
console.log( realMap.get(true) ); // 'zzinsoleKim'
console.log( realMap.size ); // 3
맵의 key에 중복값이 있으면 객체와 마찬가지로 나중값으로 적용된다.
map [key]는 Map을 쓰는 바른 방법이 아니다.
map[key] = 10으로 값을 설정하는 것 같이 map [key]를 사용할 수 있지만, 이 방법은 map을 일반 객체 취급한다.
map을 사용할 땐 map 전용 메서드 set, get 등을 사용해야만 한다.

 


Map은 키로 객체도 허용

 

✨ 객체

let obj = { name: "Object" };

let visitsCountObj = {}; // 객체를 하나 만듭니다.

visitsCountObj[obj] = 123; // 객체(obj)를 키로 해서 객체에 값(123)을 저장해봅시다.

// 원하는 값(123)을 얻으려면 아래와 같이 키가 들어갈 자리에 `object Object`를 써줘야합니다.
console.log( visitsCountObj["[object Object]"] ); // 123

 

객체를 객체의 키로 사용하면 JavaScript는 자동으로 문자열로 변환하여 사용한다.

객체를 문자열로 변환하면 기본적으로 문자열 "[object Object]"가 생성된다.

 

 

 ✨ Map

let Kim = { name: "zzinsole" };

// 고객의 가게 방문 횟수를 세본다고 가정.
let visitsCount = new Map();

// Kim을 맵의 키로 사용.
visitsCount.set(Kim, 119);

console.log(visitsCount.get(Kim)); // 119

맵은 객체와 달리 키를 문자형으로 변환하지 않는다. 키엔 자료형 제약이 없다.

 


체이닝

 

map.set을 호출할 때마다 맵 자신이 반환된다. 이를 이용해 map.set을 체이닝 할 수 있다.

let list = new Map();
list.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1');
console.log(list); // Map { '1' => 'str1', 1 => 'num1', true => 'bool1' }

 


Map의 요소에 반복 작업하기

 

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()) {
  console.log(vegetable); // cucumber, tomatoes, onion
}

// 값(amount)을 대상으로 순회.
for (let amount of recipeMap.values()) {
  console.log(amount); // 500, 350, 50
}

// [키, 값] 쌍을 대상으로 순회.
for (let entry of recipeMap) { // recipeMap.entries()와 동일합니다.
  console.log(entry); // [ 'cucumber', 500 ],..
}
맵은 삽입 순서를 기억한다. (이터러블 객체)
맵은 값이 삽입된 순서대로 순회를 실시한다.
객체가 프로퍼티 순서를 기억하지 못하는 것과는 다릅니다.

 

여기에 더하여 맵은 배열과 유사하게 내장 메서드 forEach도 지원한다.

// 각 (키, 값) 쌍을 대상으로 함수를 실행
recipeMap.forEach( (value, key, map) => {
  console.log(`${key}: ${value}`); // cucumber: 500,..
});

 


객체 ➡️ Map으로 바꾸기

 

평범한 객체를 가지고 맵을 만들고 싶다면 내장 메서드 Object.entries(obj)를 활용해야 한다.

이 메서드는 객체의 키-값 쌍을 요소([key, value])로 가지는 배열을 반환한다.

 

// 그냥 맵 만들기 (각 요소가 [키, 값] 쌍인 배열)
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);


// 객체로 맵 만들기
let obj = {
  name: "Bee",
  age: 5
};

let map2 = new Map(Object.entries(obj)); // 객체를 2차원의 키:밸류 형태로 만들고 맵으로 변환
                                         // [ ["name","Bee"], ["age", 5] ]
console.log(map2.get('name')); // Bee

 

 

Map ➡️ 객체로 바꾸기

 

Object.fromEntries를 사용하면 가능합니다. 이 메서드는 각 요소가 [키, 값] 쌍인 배열을 객체로 바꿔준다.

자료가 맵에 저장되어 있는데, 서드파티 코드에서 자료를 객체형태로 넘겨받길 원할 때 이 방법을 사용.

 

서드파티 코드: 개발자가 직접 작성하지 않은 코드. 다른 회사나 개인이 개발한 라이브러리, 모듈, 프레임워크

 

 

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries()); // 맵을 일반 객체로 변환 (*)
// let obj = Object.fromEntries(map); // .entries()를 생략할 수 있음.
// 맵이 객체가 되었습니다!
// obj = { banana: 1, orange: 2, meat: 4 }

console.log(obj.orange); // 2

 


Set

 

Set은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이다.

Set에 키가 없는 값이 저장된다.

 

let set = new Set(["A", "B", "C", "A", "c"]); 
console.log(set);// Set { 'A', 'B', 'C', 'c' }

 

new Set(iterable) 셋을 만든다
이터러블 객체를 전달받으면 그 안의 값을 복사해 셋에 넣어준다
set.add(value) 값을 추가하고 셋 자신을 반환
set.delete(value) 값을 제거한다
호출 시점에 셋 내에 값이 있어서 제거에 성공하면 true 아니면 false 반환
set.has(value) 셋 내에 값이 존재하면 true, 아니면 false를 반환
set.clear() 셋을 비운다
set.size 셋에 몇 개의 값이 있는지 세준다

 

셋 내에 동일한 값(value)이 있다면 set.add(value)를 아무리 많이 호출해도 아무런 반응이 없다.

셋 내의 값에 중복이 없는 이유가 바로 이 때문이다.

 

방문자 방명록을 만든다고 가정해 보자.

한 방문자가 여러 번 방문해도 중복해서 기록하지 않겠다고 결정한 상황.

즉, 단 한번만 기록되어야 할 때 적합한 자료구조가 바로 Set이다.

 

let set = new Set();

let ggangzi = { name: "ggangzi" };
let zzinsole = { name: "zzinsole" };
let seunghyun = { name: "seunghyun" };

// 어떤 고객(zzinsole, seunghyun)은 여러 번 방문할 수 있습니다.
// 체이닝
set
  .add(ggangzi)
  .add(zzinsole)
  .add(seunghyun)
  .add(zzinsole)
  .add(seunghyun);

// 셋에는 유일무이한 값만 저장됩니다.
console.log(set.size); // 3

for (let user of set) {
console.log(user.name); // // ggangzi, zzinsole, seunghyun 순으로 출력됩니다.
}
Set 대신 배열을 사용하여 방문자 정보를 저장한 후, 중복 값 여부는 배열 메서드인 arr.find를 이용해 확인 가능.
하지만 arr.find는 배열 내 요소 전체를 뒤져 중복 값을 찾기 때문에, Set보다 성능 면에서 떨어짐.
반면, Set은 값의 유일무이함을 확인하는데 최적화.

 


Set의 값의 반복 작업

 

for of, forEach를 사용하면 셋의 값을 대상으로 반복 작업을 수행할 수 있다.

 

  let set = new Set(["🧅", "🛌", "🦈"]);

  for (let value of set) console.log(value); // 🧅, 🛌, 🦈
  
  // forEach를 사용해도 동일하게 동작합니다.
  set.forEach((value, valueAgain, set) => {
    console.log(value); // 🧅, 🛌, 🦈
  });

 

forEach에 쓰인 콜백 함수는 세 개의 인수를 받는데, 첫 번째는 값, 두 번째도 같은 값인 valueAgain을 받는다.

세 번째는 목표하는 객체(Set)이다. 동일한 값이 인수에 두 번 등장하고 있다.

 

이렇게 구현된 이유는 Map과의 호환성 때문이다. Map의 forEach에 쓰인 콜백이 세 개의 인수를 받을 때를 위해서이다. 이렇게 구현해 놓았기 때문에 Map을 Set으로, 혹은 Set을 Map으로 교체하기 쉽다.

 

Set에도 Map과 마찬가지로 반복 작업을 위한 메서드가 있다.

 

set.keys() 셋 내의 모든 값을 포함하는 이터러블 객체를 반환
set.values() set.keys와 동일한 작업을 함
맵과의 호환성을 위해 만들어진 메서드
set.entries() 셋 내의 각 값을 이용해 만든 [value, value] 배열을 포함하는
이터러블 객체를 반환, 맵과의 호환성을 위해 만들어진 메서드