Published on

맵과 셋

Authors
  • avatar
    Name
    Na Hyunwoo
    Twitter

지금까지는 아래와 같은 자료구조들을 많이 봐왔습니다.

  • 객체 - 키가 있는 컬렉션을 저장함
  • 배열 - 순서가 있는 컬렉션을 저장함

하지만 이 두 자료구조 만으론 부족해 ES6부터 맵(Map)과 셋(Set)이 등장하게 되었습니다.

이 블로그 포스팅은 맵 - 객체, 셋 - 배열의 개념을 확실히 구분하고 맵과 셋을 더 잘 사용할 수 있기 위함입니다.

은 키가 있는 데이터를 저장한다는 점에서 객체와 유사합니다. 다만, 맵은 키에 다양한 자료형이 올 수 있다는 차이점이 있습니다.

맵의 시간 복잡도

  • map은 해시 테이블 자료구조를 사용하기 때문에 set, delete와 같은 연산은 O(1)의 시간 복잡도를 가집니다.
map-set-map-algorithm

맵의 주요 메서드와 프로퍼티

  • new Map() - 맵을 만듭니다.
  • map.set(key, value) - key를 이용해 value를 저장합니다.
  • map.get(key) - key에 해당하는 value를 반환합니다. key가 존재하지 않으면 undefined를 반환합니다.
  • map.has(key) - key 가 존재하면 true, 존재하지 않으면 false를 반환합니다.
  • map.delete(key) - key에 해당하는 값을 삭제합니다.
  • map.clear() - 맵 안의 모든 요소를 제거합니다.
  • map.size - 요소의 개수를 반환합니다.

맵 사용시 주의할 점

map-set-caution

맵의 반복 작업

  • map.keys() - 각 요소의 키를 모은 iterable 객체를 반환합니다.
  • map.values() - 각 요소의 값을 모은 iterable 객체를 반환합니다.
const recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion', 50],
])

for (const vegetable of recipeMap.keys()) {
  alert(vegetable)
}

for (const amount of recipeMap.values()) {
  alert(amount)
}

for (const entry of recipeMap) {
  alert(entry)
}

맵은 삽입 순서를 기억합니다.

맵은 배열과 유사하게 forEach를 지원합니다.

recipeMap.forEach((value, key, map) => {
  alert(`${key}: ${value}`)
})

객체를 맵으로 바꾸기

키-값 쌍인 배열이나 이터러블 객체를 초기화 용도로 맵에 전달해 새로운 맵을 만들 수 있습니다.

const map = new Map([
  ['1', 'str1'],
  [1, 'num1'],
  [true, 'bool1'],
])

alert(map.get('1')) //str1

내가 궁금했던 것

객체를 배열로 가지는 녀석을 Map으로 만들 수 있을까 ?

읽어들이질 못한다.

map-set-obj

반면에 배열의 요소로 배열은 잘 읽어들인다.

map-set-arr

셋은 중복을 허용하지 않는 값을 모아놓은 컬렉션입니다. 배열과 유사합니다.

셋의 시간 복잡도

set 또한 해시 테이블을 사용하였기 때문에 has와 같은 연산은 O(1)의 시간 복잡도를 가집니다.

셋의 주요 메서드

  • new Set(iterable) - 셋을 만듭니다.
  • set.add(value) - 값을 추가하고 셋 자신을 반환합니다.
    • set.set(value)은 이상하니까 set.add(value)가 더 자연스러운 흐름이겠다.
  • set.delete(value) - 값을 제거합니다.
  • set.has(value) - 셋 내에 값이 존재하면 true, 아니면 false를 반환합니다.
  • set.clear() - 셋을 비웁니다.
  • set.size - 셋에 몇 개의 값이 있는지 세줍니다.
const set = new Set()

const john = { name: 'John' }
const pete = { name: 'Pete' }
const mary = { name: 'Mary' }

set.add(john)
set.add(pete)
set.add(mary)
set.add(john)
set.add(mary)

alert(set.size) // 3

for (const user of set) {
  alert(user.name) // John, Pete, Mary 순으로 출력됩니다.
}

셋의 값에 반복 작업하기

for..of나 forEach를 사용하여 반복 작업을 수행할 수 있습니다.

const set = new Set(['oranges', 'apples', 'bananas'])

for (const value of set) alert(value)

set.forEach((value, valueAgain, set) => {
  alert(value)
})

이것은 특이하게도 value, valueAgaing과 같이 동일한 값이 인수에 두 번 등장하고 있습니다.

이렇게 구현된 이유는 과의 호환성 때문입니다. 맵의 forEach에 쓰인 콜백이 세 개의 인수를 받을 때를 위해서입니다. 이렇게 구현하였기 때문에 맵과 셋의 교체가 쉬워집니다.

결론

  • 맵과 셋은 기존의 객체와 배열의 한계를 극복하기 위해 나온 자료구조입니다.
  • 둘 다 모두 해시 테이블 자료구조를 사용합니다. 따라서, 시간 복잡도에서 우위를 가져갑니다.

직접 해보기

배열에서 중복 요소 제거하기 (중요도 5)

function unique(arr) {
  /* 제출 답안 */
}

let values = ['Hare', 'Krishna', 'Hare', 'Krishna', 'Krishna', 'Krishna', 'Hare', 'Hare', ':-O']

alert(unique(values)) // 얼럿창엔 `Hare, Krishna, :-O`만 출력되어야 합니다.

풀어보기

애너그램 걸러내기 (중요도 4)

  • 애너그램은 단어나 문장을 구성하고 있는 문자의 순서를 바꾸어 다른 단어나 문자를 만드는 놀이입니다.
// 애너그램의 예
nap - pan
ear - are - era
cheaters - hectares - teachers
let arr = ['nap', 'teachers', 'cheaters', 'PAN', 'ear', 'era', 'hectares']

alert(aclean(arr)) // "nap,teachers,ear"나 "PAN,cheaters,era"이 출력되어야 합니다.

풀어보기

반복 가능한 객체의 키 (중요도 5)

map.keys()를 사용해 배열을 반환하고, 이 배열을 변수에 저장해 .push와 같은 배열 메서드를 적용하고 싶다고 해봅시다.

작동하지 않습니다.

let map = new Map()

map.set('name', 'John')

let keys = map.keys()

// Error: keys.push is not a function
keys.push('more')

이유가 무엇일까요 ? keys.push가 작동하게 하려면 코드를 어떻게 고쳐야 할까요 ?

풀어보기

직접 해보기 답

function unique(arr) {
  return Array.from(new Set(arr))
}
const arr = ['nap', 'teachers', 'cheaters', 'PAN', 'ear', 'era', 'hectares']

alert(aclean(arr)) // "nap,teachers,ear"나 "PAN,cheaters,era"이 출력되어야 합니다.

function aclean(arr) {
  // 모두 소문자로 변환한다.
  // 문자열들을 배열로 쪼갠 뒤 알파벳 순으로 정렬한 뒤 다시 문자열로 변환한다.
  // 맵에 정렬한 알파벳과 원본을 key, value 쌍으로 넣는다.

  const map = new Map()

  // 키가 동일한 값으로 덮어씌워지기 때문에, 글자 구성이 같은 단어는 단 한 번만 저장됩니다.
  for (const word of arr) {
    const sorted = word.toLowerCase().split('').sort().join('')
    map.set(sorted, word)
  }

  return Array.from(map.values())
}
// keys.push가 동작하지 않는 이유는 map.keys()가 배열이 아니라 이터러블을 반환하기 때문입니다.
// Array.from을 이용하여 맵을 배열로 변환할 수 있습니다.

const map = new Map()

map.set('name', 'John')

const keys = Array.from(map.keys())

keys.push('more')

alert(keys) // name, more

참고

이터러블

  • for..of를 사용할 수 있는 객체를 이터러블이라고 부릅니다.
  • 대표적인 이터러블의 예로는 배열, 문자열이 있습니다.

유사 배열

  • length 프로퍼티가 있는 객체는 유사 배열이라고 불립니다.

Array.from을 사용하여 이터러블과 유사 배열인 obj를 진짜 Array로 만들 수 있습니다.

해시와 관련된 문제를 더 풀고 싶다면 이곳에서 풀어보세요 !

레퍼런스

맵과 셋

iterable 객체

Javascript map deep dive