본문 바로가기
프론트엔드/JS 공부

Map() 객체에 대한 정리

by 학습하는 청년 2024. 4. 4.

최종 수정 : 2024-04-04

필요한 지식

- 추후 링크 연결 예정
1) 이터러블 / 이터레이터

2) 생성자 함수

3) prototype

 

Map 객체는 키와 값의 쌍으로 이루어진 컬렉션이다. 객체와 유사한 특징은 있지만 차이가 있다.

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

1. Map 객체의 생성

맵은 항상 명시적으로 새로운 인스턴스를 생성해야 한다.

const map = new Map();
console.log(map); // Map(0) {} 빈 객체

 

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

// 이렇게 변경해야 된다.
const map2 = new Map([[1, 2]]); // Map(1) {1 => 2}

 

Map은 중복된 키를 허용하지 않는다. 다시 말해, 값이 갱신된다는 의미이다.

const Map = new Map([['key1', 'value'], ['key2', 'value2']]);
console.log(map); // Map(1) {"key1" => "value2"}

2. 메소드

1) set()

set('key', 'value')를 통해 키와 값을 갖는 프로퍼티를 추가할 수 있다. 또한 체이닝을 통해 연속적으로 요소를 추가할 수 있다. Map은 중복 키를 허용하지 않기 때문에, 나중에 값이 해당 키와 쌍을 이룬다.

 

Map은 객체와 달리 객체를 포함한 모든 값을 키(key)로 설정할 수 있다.

NaN === NaN과 +0 === -0 은 false이지만, Map 객체는 같다고 평가하여 같은 키 값으로 중복 추가가 허용되지 않는다.
const map = new Map();

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

// 체이닝(method chaining)과 중복
map.set('key2', 'value2').set('key2', 'value3'); 
// Map(2) {"key1" => "value1", "key2" => "value3"}

 

2) 요소 수정 : get() / has() / delete() / clear()

get() : 요소의 키를 인자로 요소의 값이 반환된다.
has() : 요소의 키를 인자로 존재 여부가 boolean 값으로 반환된다.
delete() : 요소의 키를 인자로 키와 값을 삭제되며 boolean 값이 반환된다. 따라서 체이닝이 불가능하다.
clear() : Map의 모든 키-값 쌍을 삭제한다. 언제나 undefined를 반환한다.
const map = new Map();

const js = { name: 'JavaScript' };
const ts = { name: 'Typescript' };

// 추가
map
  .set(js, 'language1')
  .set(ts, 'language2');

// 확인
map.get(js); // language1
map.get('key'); // undefined
  
// 존재여부 확인
map.has(js); // true
map.has('key'); // false

// 삭제
map
  .delete(js); // true, Map(1) { {name: 'Ty[eScript'} => 'language2' }
  .delete(ts); // false, TypeError
  
// 모두 삭제
map.clear(); // undefined, Map(0) {}

 

3) 요소순회 : key(), values(), entries() 

Map 객체는 이터러블이면서 동시에 이터레이터인 객체를 반환하는 메서드를 제공한다.

keys() : 요소 키를 값으로 반환
values() : 요소의 값을 반환
entries() : 요소의 키와 값을 쌍으로 반환

 

Map 객체는 요소의 순서에 의미를 갖지 않지만, 순회하는 순서는 요소가 추가된 순서를 따른다. 그 이유는 ECMAScript 사양에 규정되어 있지는 않지만 다른 이터러블의 순회와 호환성을 유지하기 위함이다.

const js = { name: 'JavaScript' };
const ts = { name: 'TypeScript' };

const map = new Map([[js, 'language1'], [ts, 'language2']]);

// {name: "JavaScript"} {name: "TypeScript"}
for (const key of map.key()) { console.log(key); };

// language1 language2
for (const value of map.values()) { console.log(value); };

// [{name: "JavaScript}, "language1"] [{name: "TypeScript"}, "language2"]
for (const entry of map.entries()) { console.log(entry); };

3. 요소 순회

1) forEach()

Array.forEach 메서드와 유사하게 콜백 함수와 forEach 메서드의 콜백 함수 내부에서 this로 사용될 객체(옵션)를 인수로 전달한다.

첫 번째 인수: 순회 중인 요소값
두 번째 인수: 순회 중인 요소키
세 번째 인수: 순회 중인 Map 객체 자체
const js = { name: 'JavaScript' };
const ts = { name: 'TypeScript' };

const map = new Map([[js, 'language1'], [ts, 'language2']]);

// forEach()
map.forEach((v, k, map) => console.log(v, k, map));
/*
language1 {name: "JavaScript"} Map(2) {
  {name: "js"} => "language1",
  {name: "ts"} => "language2"
}
language2 {name: "TypeScript"} Map(2) {
  {name: "js"} => "language1",
  {name: "ts"} => "language2"
}
*/

2) for ... of

Map 객체는 이터러블이다. 따라서 for ... of 문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트럭처링 할당의 대상이 될 수도 있다.

const js = { name: 'JavaScript' };
const ts = { name: 'TypeScript' };

const map = new Map([[js, 'language1'], [ts, 'language2']]);

// [1] Map 객체는 Symbol.iterator 메서드를 상속받는 이터러블
console.log(Symbol.iterator in map); // true

// [2] for ... of문으로 순회
for (const entry of map) { console.log(entry); };

// [3] 스프레드 문법의 대상
console.log([...map]);

// [4] 배열 디스트럭처링 할당의 대상
const [a, b] = map;
console.log(a, b); 

// [1] ~ [4] 모두 
// --> [{name: "JavaScript"}, "language1"] [{name: "TypeScript"}, "language2"]

 

4. 객체가 있는데, Map()이 추가된 이유는 무엇인가?

1) 인터페이스가 명확하다.

2) 예측 가능한 이름의 메서드를 갖고 있다.

3) 반복과 같은 동작이 내장되어 있다.

function addFilters(filters, key, value) {
  filters[key] = value;
}

function deleteFilters(filters, key) {
  delete filters[key];
}

function clearFilters(filters) {
  filters = {};
  return filters;
}

일반적으로 객체의 정보를 갱신하려면 '필터링 조건 추가', '필터링 조건 삭제', '모든 조건 제거' 라는 세 가지 동작이 필요하다. 문제는 단지 기본적인 동작을 수행하는 데도, 서로 다른 세 가지 패러다임을 적용한다는 점이다. 마지막 같은 경우, 실제로는 filter = new Object();를 한 것과 다름 없다.

 

맵의 메소드를 이용하면, 객체 대신 맵을 사용하도록 함수를 변경할 수 있다.

const languageFilters = new Map();
function addFilters(filters, key, value) {
  filters.set(key, value);
}
function deleteFilters(filters, key) {
  filters.delete(key);
}
function clearFilters(filters) {
  filters.clear();
}

코드가 훨씬 명료하게 보인다. 그 자체만으로도 큰 이점이다. 새로 작성한 함수에서는 다음과 같은 특징이 있다.

1) 맵 인스턴스에 항상 메서드를 사용한다.

2) delete() 메서드를 사용할 수 있기에, 인스턴스를 생성한 후에는 언어 수준의 연산자를 섞지 않는다.

3) clear() 메서드를 사용할 수 있기에, 새로운 인스턴스를 생성할 필요가 없다.


5. 객체와의 차이점

맵은 특정 작업을 매우 쉽게 처리하는 특별한 종류의 컬렉션이다. 

 

1) 키-값 쌍이 자주 추가되거나 삭제되는 경우

정보를 자주 변경하는 경우에는 객체보다 맵을 사용하는 것이 훨씬 편리하다. 모든 동작과 의도가 명료하게 보이기 때문이다. 

 

2) 키가 문자열이 아닌 경우

객체와 달리, 맵은 숫자를 포함해 여러 가지 자료형을 키(key)로 사용할 수 있다.

let language = new Map([
  [100, 'JavaScript'],
  [200, 'TypeScript'],
  [300, 'React']
]);

// language.get(100); // 'JavaScript'

language.keys(); // MapIterator { 100, 200, 300 }
language.entries(); // MapIterator { [100, 'JavaScript'], [200, 'TypeScript'], [300, 'React'] }

객체에 Object.keys()를 적용한 것과 다르게 배열이 반환되지 않는다. 반환된 값은 맵이터레이터(MapIterator) 라고 부른다. 이를 통해 데이터를 순회할 수 있다. 맵은 정렬과 순회에 필요한 기능이 내장되어 있는데, 이는 맵이터레이터의 일부로 포함되어 있다.

 

일반 객체는 for .. of 반복문을 통해 키-값 쌍의 이터레이터를 반환하지만, entries() 메서드를 사용하면, 맵에 있는 키-값을 쌍으로 묶은 맵이터레이터를 반환한다. 이 기능은 매우 편리하기 때문에, ES2017부터 객체의 내장 메서드로 추가됐다. 이처럼 맵을 직접 순회할 수 있기 때문에, 키를 먼저 꺼낼 필요가 없다. 게다가 맵을 순회할 때 키-값 쌍을 받아서 해체 할당 문법으로 즉시 변수로 할당할 수 있다.

function getAplliedFilters(language) {
  const applied = [];
  for (const [key, value] of language) {
    applied.push(`${key}:${value}`);
  }
  return `선택한 언어는 ${applied.join(', ')} 입니다.`;
}

// '선택한 언어는 100:JavaScript, 200:TypeScript, 300:React 입니다.'

맵은 순서를 저장하기 때문에, 맵의 첫 번째 항목을 첫 번째로 받는다. 반면 정렬 메서드가 내장되어 있지 않는다. 하지만. 이는 펼침 연산자를 이용하면 보완할 수 있다. 맵 객체의 경우에는 키-값 쌍이 반환된다. 이를 활용하면 다음과 같다.

function getAppliedFilters(language) {
  const applied = [...filter].map(([key, value]) => {
    return `${key}:${value}`;
  });
  return `선택한 조건은 ${applied.join(', ')} 입니다`;
}

 

맵은 객체와 마찬가지로 하나의 키를 한 번만 사용할 수 있다. 즉, 새로운 키로 맵을 생성하면 어떤 값이든 해당 키에 마지막으로 선언한 값을 사용한다. 갱신하는 것이다.

const filters = new Map()
  .set('A', 'JavaScript')
  .set('A', 'TypeScript');

filters.get('A'); // TypeScript

// 아래의 경우도 같다.
let js = new Map()
  .set('A', 'JavaScript');
  
let ts = new Map()
  .set('A', 'TypeScript');

let update = new Map([...js, ,,,ts]);
update.get('A'); // TypeScript

 

이를 함수로 바꾸면 다음과 같다.

function language(js, ts) {
  return new Map([...js, ...ts]);
}

참고 자료

자바스크립트 코딩의 기술 (p.95-115)

- Tip 13 맵으로 명확하게 키-값 데이터를 갱신하라

- Tip 14 맵과 펼침 연산자로 키-값 데이터를 순회하라

- Tip 15 맵 생성 시 부수 효과를 피하라

 

모던 자바스크립트 Deep Dive (p.653-659)

댓글