자바스크립트 맵(Map) 완벽 가이드

자바스크립트 맵(Map) 완벽 가이드

자바스크립트에서 키와 값의 쌍으로 데이터를 저장해야 할 때, 대부분 일반 객체({})를 사용하실 텐데요. 객체가 워낙 범용적이다 보니 많은 분들이 별 생각 없이 객체로 모든 키-값 저장을 해결하곤 합니다.

그런데 ES6에서 추가된 맵(Map)을 사용하면 객체로는 까다로운 작업들을 훨씬 깔끔하게 처리할 수 있습니다. 키의 타입에 제한이 없고 데이터의 개수를 바로 알 수 있으며 삽입 순서가 보장되는 등 여러 장점이 있거든요.

이번 포스팅에서는 다양한 예제를 통해서 자바스크립트의 Map을 어떻게 사용하는지 차근차근 알아보겠습니다.

맵 자료구조

자바스크립트의 Map을 살펴보기 전에, 먼저 자료구조로서의 맵(map)이 무엇인지 간단히 짚고 넘어가겠습니다.

맵(map)은 키(key)와 값(value)의 쌍을 저장하는 자료구조입니다. 사전(dictionary)이나 해시 테이블(hash table)이라고 불리기도 하는데요. 핵심은 하나의 키에 하나의 값이 대응되며, 키를 통해 값을 빠르게 조회할 수 있다는 점입니다.

예를 들어 전화번호부를 떠올려보면 이해가 쉽습니다. 이름(키)으로 전화번호(값)를 찾는 구조죠.

자바스크립트에서는 일반 객체도 키-값 저장 용도로 많이 쓰이지만 객체의 키는 문자열과 심볼(Symbol)로 제한됩니다. 반면 Map은 어떤 타입이든 키로 사용할 수 있어서 좀 더 유연한 키-값 저장이 가능합니다.

맵 생성

자바스크립트에서 Map은 클래스이므로 new 키워드와 생성자를 사용하여 맵 객체를 생성합니다.

const map = new Map(); // Map(0) {size: 0}

생성자에 아무것도 넘기지 않으면 빈 맵이 만들어지고 [키, 값] 형태의 배열을 원소로 갖는 이차원 배열을 넘기면 초기 데이터가 담긴 맵이 만들어집니다.

const map = new Map([
  ["name", "Dale"],
  ["city", "Seoul"],
  ["lang", "JavaScript"],
]);
// Map(3) {'name' => 'Dale', 'city' => 'Seoul', 'lang' => 'JavaScript'}

값 저장

맵에 키-값 쌍을 추가하거나 기존 키의 값을 변경할 때는 set() 메서드를 사용합니다.

const map = new Map();
map.set("name", "Dale"); // Map(1) {'name' => 'Dale'}
map.set("city", "Seoul"); // Map(2) {'name' => 'Dale', 'city' => 'Seoul'}

이미 존재하는 키로 set()을 호출하면 값이 덮어씌워집니다.

map.set("city", "Busan");
// Map(2) {'name' => 'Dale', 'city' => 'Busan'}

참고로 set() 메서드는 맵 자신을 반환하기 때문에 아래와 같이 연쇄적으로 호출할 수도 있습니다.

const map = new Map()
  .set("name", "Dale")
  .set("city", "Seoul")
  .set("lang", "JavaScript");

일반 객체와 가장 크게 다른 점 중 하나가 바로 키의 타입인데요. 객체의 키는 문자열이나 심볼만 가능하지만 맵은 어떤 값이든 키로 사용할 수 있습니다.

const map = new Map();

// 숫자 키
map.set(1, "one");
map.set(2, "two");

// 불리언 키
map.set(true, "참");

// 객체 키
const user = { name: "Dale" };
map.set(user, "admin");

// 함수 키
const greet = () => "hello";
map.set(greet, "인사 함수");

DOM 요소를 키로 사용해서 그 요소에 관련된 데이터를 맵에 저장하는 것도 실무에서 종종 볼 수 있는 패턴입니다.

const metadata = new Map();
const button = document.querySelector("#submit-btn");
metadata.set(button, { clickCount: 0, lastClicked: null });

값 읽기

맵에서 특정 키에 대응하는 값을 가져오려면 get() 메서드를 사용합니다. 해당 키가 맵에 없으면 undefined가 반환됩니다.

const map = new Map([
  ["name", "Dale"],
  ["city", "Seoul"],
]);

map.get("name"); // 'Dale'
map.get("city"); // 'Seoul'
map.get("age"); // undefined

값 존재 여부 확인

맵에 특정 키가 있는지 확인하려면 has() 메서드를 사용합니다. true 또는 false를 반환하므로 if 조건문과 함께 쓰면 편리합니다.

if (map.has("name")) {
  console.log(`이름: ${map.get("name")}`); // 이름: Dale
}

map.has("age"); // false

일반 객체에서 속성 존재 여부를 확인하려면 in 연산자나 hasOwnProperty() 메서드를 써야 해서 좀 번거로운데요. 맵의 has() 메서드는 직관적이고 사용하기 편합니다.

값 삭제

맵에서 특정 키-값 쌍을 삭제하려면 delete() 메서드를 사용합니다. 삭제에 성공하면 true, 해당 키가 없으면 false를 반환합니다.

map.delete("city"); // true
map.delete("phone"); // false

크기 확인

맵에 저장된 키-값 쌍의 개수는 size 속성으로 확인할 수 있습니다.

const map = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);
console.log(map.size); // 3

일반 객체에는 이런 속성이 없어서 Object.keys(obj).length처럼 번거롭게 키 배열을 만든 다음에 길이를 구해야 하는데요. 맵을 쓰면 이런 수고가 필요 없습니다.

모든 값 제거

맵의 모든 키-값 쌍을 한번에 제거하려면 clear() 메서드를 사용합니다.

map.clear(); // Map(0) {size: 0}

맵 순회

맵에 저장된 데이터를 순회하는 방법은 여러 가지가 있습니다. 상황에 맞게 골라서 사용하시면 돼요.

for...of 루프를 사용하면 각 반복에서 [키, 값] 형태의 배열을 받을 수 있습니다. 구조 분해 할당을 활용하면 키와 값을 바로 변수로 꺼낼 수 있어서 편리합니다.

const map = new Map([
  ["name", "Dale"],
  ["city", "Seoul"],
  ["lang", "JavaScript"],
]);

for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}
// name: Dale
// city: Seoul
// lang: JavaScript

forEach() 메서드로도 순회할 수 있는데, 콜백 함수의 첫 번째 인자가 값이고 두 번째 인자가 키라는 점에 주의하세요.

map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

키만 필요하거나 값만 필요한 경우에는 keys()values() 메서드를 사용할 수 있습니다.

for (const key of map.keys()) {
  console.log(key); // name, city, lang
}

for (const value of map.values()) {
  console.log(value); // Dale, Seoul, JavaScript
}

entries() 메서드는 [키, 값] 쌍의 이터레이터를 반환하는데, 사실 맵을 for...of로 직접 순회하는 것과 동일한 결과를 얻습니다.

for (const [key, value] of map.entries()) {
  console.log(`${key}: ${value}`);
}

맵과 배열 변환

맵을 배열로 변환할 때는 전개(spread) 연산자를 사용하면 간단합니다.

const map = new Map([
  ["a", 1],
  ["b", 2],
]);

const entries = [...map]; // [['a', 1], ['b', 2]]
const keys = [...map.keys()]; // ['a', 'b']
const values = [...map.values()]; // [1, 2]

Array.from()을 써도 같은 결과를 얻을 수 있습니다.

const entries = Array.from(map); // [['a', 1], ['b', 2]]

반대로 [키, 값] 형태의 이차원 배열을 Map 생성자에 넘기면 맵으로 변환됩니다. 이 점을 활용하면 Object.entries()로 객체를 맵으로 바꿀 수도 있습니다.

const obj = { name: "Dale", city: "Seoul" };
const map = new Map(Object.entries(obj));
// Map(2) {'name' => 'Dale', 'city' => 'Seoul'}

맵을 다시 객체로 바꿀 때는 Object.fromEntries()를 쓰면 됩니다.

const obj = Object.fromEntries(map);
// { name: 'Dale', city: 'Seoul' }

객체 대신 맵을 써야 할 때

자바스크립트에서 키-값 쌍을 저장할 때 일반 객체와 맵 중에 뭘 써야 하는지 고민되실 수 있는데요. 둘 다 비슷한 역할을 하지만 다음과 같은 상황에서는 맵이 확실히 더 적합합니다.

우선 키가 문자열이 아닌 경우에는 맵이 필수입니다. 객체의 키는 문자열(또는 심볼)만 가능하지만 맵은 객체나 숫자, 함수 등 어떤 값이든 키로 쓸 수 있습니다.

// 객체 키를 써야 하는 경우 - Map만 가능
const permissions = new Map();
permissions.set(adminUser, ["read", "write", "delete"]);
permissions.set(guestUser, ["read"]);

그리고 데이터를 자주 추가하거나 삭제하는 경우에도 맵이 유리합니다. 맵은 이런 연산에 최적화되어 있어서 객체보다 성능이 좋습니다.

또한 데이터의 개수를 자주 확인해야 한다면 맵의 size 속성이 편리합니다. 객체는 Object.keys(obj).length를 매번 계산해야 하니까요.

마지막으로 삽입 순서가 중요한 경우에도 맵이 안전합니다. 맵은 항상 삽입 순서대로 순회가 보장되는 반면 객체는 키가 정수처럼 생긴 경우에 순서가 달라질 수 있습니다.

반대로 JSON으로 직렬화해야 하거나 구조가 고정된 레코드를 다룰 때는 일반 객체가 더 자연스럽습니다. 맵은 JSON.stringify()로 직접 변환할 수 없어서 별도의 처리가 필요하거든요.

Map.groupBy()

ES2024에서 추가된 Map.groupBy() 정적 메서드를 사용하면 배열의 데이터를 특정 기준으로 그룹화하여 맵에 담을 수 있습니다.

const users = [
  { name: "Dale", age: 30, role: "admin" },
  { name: "John", age: 25, role: "user" },
  { name: "Jane", age: 28, role: "admin" },
  { name: "Robin", age: 22, role: "user" },
];

const usersByRole = Map.groupBy(users, ({ role }) => role);
console.log(usersByRole.get("admin"));
// [{name: 'Dale', age: 30, role: 'admin'}, {name: 'Jane', age: 28, role: 'admin'}]

Object.groupBy()도 있지만 키가 문자열이 아닌 값으로 분류해야 할 때는 Map.groupBy()가 더 적합합니다. 이 API에 대해서 더 자세히 알고 싶으시다면 groupBy() API 사용법을 참고하세요.

타입스크립트에서 맵 사용

타입스크립트에서는 맵을 생성할 때 키와 값의 타입을 지정할 수 있는데요.

const map = new Map<string, number>();
map.set("age", 30);
map.set("age", "thirty"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

좀 더 복잡한 타입도 적용할 수 있습니다.

interface User {
  name: string;
  age: number;
}

const userMap = new Map<number, User>();
userMap.set(1, { name: "Dale", age: 30 });
userMap.set(2, { name: "John", age: 25 });

이렇게 타입을 명시해두면 코드 편집기의 자동완성 지원도 받을 수 있고 잘못된 타입의 데이터를 저장하려 할 때 컴파일 단계에서 오류를 잡아주니 훨씬 안전하겠죠.

마치며

지금까지 자바스크립트에서 맵을 사용하는 방법에 대해서 자세히 살펴보았습니다. 맵은 객체와 비슷하면서도 키 타입의 유연성, size 속성, 삽입 순서 보장 등의 이점이 있어서 상황에 맞게 잘 활용하면 더 깔끔하고 효율적인 코드를 작성할 수 있습니다.

맵과 함께 ES6에서 추가된 세트(Set)도 알아두시면 자바스크립트의 자료구조 활용 폭이 한층 넓어질 거예요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord