Collections

프로그래밍 언어에서 Collection 이란 단어는 '프로그래밍 언어가 제공하는 값을 담을 수 있는 컨테이너' 라고 생각합니다.

메이저 프로그래밍 언어에는 여러가지 데이터 컬렉션들이 존재합니다.

  • Python - list, tuple, dictionaries ...
  • Java - ArrayList, HashMap, HashSet, Queue, Stack ...
  • Ruby - hashes, arrays
  • 기타 등등

JavaScript 에서는 다음과 같은 컬렉션들이 존재합니다.

Indexed Collection - Arrays, Typed Array
Keyed Collection - Objects, Map, Set, Weak Map, Weak Set

JavaScript 에서는 ES5 때 까지는 Object와 Array만이 존재 하였습니다.

하지만 ES6 부터 Map, Set, WeakMap, WeakSet, Typed Array를 추가 하였는데요.

이 포스팅에서는 Typed Array 를 제외한 ES6에서 나온 Collection 들에 대해 이야기 하려 합니다.


왜 ES6에서 컬렉션이 추가 되었을까요?

특정 상황에서 ES6의 컬렉션들을 사용할 시 기존의 Object, Array를 사용하는 것 보다 최적화된 구현체를 제공합니다.

Set

  • Set은 value를 키 값으로 갖는 컬렉션입니다.
  • Set은 수정 가능하며, 프로그램이 실행되는 동안 값의 추가나 삭제를 할 수 있습니다.
  • 여기까지는 Array와 같지만 Set과 Array에는 차이점이 존재 합니다.

값이 중복되지 않습니다.

  • 이미 존재하는 값을 추가하려고 하면 아무 일도 일어나지 않습니다.

    const mySet = new Set("abcd");
    mySet.size; // 4
    mySet.add("a");
    mySet.size; // 4

Set은 뚜렷한 목적을 가지고 데이터를 관리합니다.

바로 어떤 데이터가 자신의 멤버인지 확인하는 작업을 빨리 처리하려는 목적입니다.

const mySet = new Set("abcd");
const myArray = [..."abcd"];

myArray.indexOf("a") !== -1 // true slow
mySet.has("a")              // true fast

Set으로 할 수 없는 일도 있습니다.

Set은 인덱스 값으로 데이터를 조회하는 일을 할 수 없습니다.

myArray[0]; // "a"
mySet[0];   // undefined

Set은 다음과 같은 메서드들을 제공합니다.

let mySet = new Set;  // 그냥 new 생성자로 빈 Set을 선언할 수 있습니다.
let iterSet = new Set([1, 2, 3]); // 생성할때 iterable한 객체를 초기 값으로 줄 수 있습니다.
console.log(iterSet.size);  // size로 Set의 크기를 알 수 있습니다.
console.log(iterSet.has(1)); // has로 Set에 값이 존재하는지 알 수 있습니다.
mySet.add(1).add(2).add(3); // add로 Set에 값을 추가합니다. 이것은 체이닝 될 수 있습니다.
mySet.delete(1).delete(2);  // delete로 Set에 값을 삭제할 수 있습니다. 역시 체이닝 될 수 있습니다. 
iterSet.forEach((value, value, iter) => v); // Array.prototype.forEach 와 같은 forEach를 제공합니다. 
iterSet.clear(); // Set의 모든 값을 삭제합니다.

Set에서 구현되지 않은 것.

  • Array.prototype에 구현된 map, filter, some, every등의 내장함수 미구현
  • 많은 값들을 한번에 처리할 수 있는 메소드들이 누락 (Ex. set.addAll(iterable), set.removeAll(iterable) 등등
  • Set에 Object 값이 들어오는 경우 레퍼런스(주소 값) 이 다르면 값이 같아도 다르게 처리됩니다.

물론 이것들은 ES6의 다른 문법들을 통해서 구현 가능합니다.


Map

Map은 Key - Value 의 쌍으로 이루어진 컬렉션입니다.

하지만 JavaScript 에는 Key - Value로 이루어진 Object가 있습니다.

그런데 왜 Map이 나온 것 일까요?

일반 객체로는 이런 문제점을 해결 할 수 없습니다.

  • 객체의 키로 내장 메소드의 이름을 사용할 시 이름 충돌이 일어날 수 있습니다.
  • 속성의 Key는 항상 문자열 이어야 합니다. (ES6 에서는 심볼도 가능합니다.)
  • 객체에 얼마나 많은 속성이 존재하는지 알아낼 수 있는 효과적인 방법이 없습니다.(sizelength 같은 메서드가 없습니다.)
  • 일반 객체를 반복하려면 많은 비용이 소모됩니다.

여기서 가장 큰 문제점은 일반 객체는 iterable 하지 않기 때문에 iterable을 사용하는 모든 문법에 객체를 사용할 수 없습니다.

하지만 직면한 문제가 위의 기능들을 필요로 하지 않는다면, 일반 객체를 사용하는 것이 올바른 선택일 수 있습니다.

왜 이런 식으로 설계 되었는가?

  • ES6 컬렉션이 사용자 데이터와 빌트인 메소드 사이의 이름 충돌을 피하기 위해 설계 되었기 때문에, ES6 컬렉션은 자신의 멤버 데이터를 드러내기 위해 속성(property)를 사용하지 않습니다.
  • 이로 인해 해시 테이블의 멤버 데이터에 접근하기 위해 obj.key 또는 obj[key] 같은 구문을 사용할 수 없습니다. 객체의 값을 가져오기 위해서는 map.get(key) 구문을 이용해야하며 해시 테이블의 멤버 데이터는 속성과 달리 프로퍼티 체인(property chain)을 통해 상속되지 않습니다.

Map은 다음과 같이 사용할 수 있습니다.

const myMap = new Map; // new 생성자로 선언해서 사용할 수 있습니다.
myMap.set('yesdoing', 'looser');

const person = { age: '111', gender: 'none', name: 'yesdoing'};
const whoami = {};

myMap.set(person, whoami); // 객체를 키와 값으로 받을 수 있습니다.

for(const [key, value] of myMap) { // destructuring으로 값을 가져와서 쓸 수 있어요.
    console.log(`${key} = ${value}`);  // yesdoing = looser [object Object] = [object Object]
}

myMap.get(person); // {}

myMap.forEach(value, key) => { // 다른 forEach와의 호완을 위해서 value, key 순입니다.
    console.log(`${key} = ${value}`);  // yesdoing = looser [object Object] = [object Object]    
}, myMap);

myMap.clear(); // map에 있는 엔트리를 전부 삭제합니다. 
console.log(myMap.size); // 0

Weak Collections

Map과 Set이 참조하는 객체들은 강하게 연결되어 있습니다. 이 것은 JavaScript의 가비지 컬렉션이 메모리 수거를 못하게 막는 원인이 됩니다. 만약 크기가 큰 Map과 Set의 객체가 더 이상 쓰이지 않는다면 가비지 컬렉션에서 이것을 가져가기 위해 비싼 비용을 치뤄야 합니다.

이것을 해결하기 위해 ES6에서는 Weak Map, Weak Set이 나왔습니다.

이 컬렉션들은 더 이상 사용되지 않을 때, 메모리에서 쉽게 삭제 되기 위해 '약한' 결합을 유지합니다.

Weak Map

Weak Map은 Map과 비슷합니다. 단지 메서드가 몇개 없고, 가비지 컬렉션의 처리가 다릅니다.

const yesdoing = new WeakMap(); // WeakMap을 생성합니다. 
const age = {}; // 키는 반드시 객체여야 합니다.
const job = {}; // 키는 반드시 객체여야 합니다.

yesdoing.set(age, 11111); // 키 - 값을 설정합니다.
yesdoing.set(job, 'air'); // 값으로는 어떤 타입이라도 들어올 수 있습니다. 

yesdoing.has(job); // True
yesdoing.delete(job) // key를 삭제합니다. 

Weak Set

Weak Set 역시 Set과 비슷하지만 메서드가 몇개 없습니다.

const yesdoing = new WeakSet(); // WeakMap을 생성합니다. 
const age = {}; // 값은 반드시 객체여야 합니다. 

yesdoing.add(age); // 값을 추가합니다.

yesdoing.has(age); // True
yesdoing.delete(age) // 값을 삭제합니다.

마무리

Map과 Set은 키 / 값의 쌍으로 이루어 진 ES6에서 새로나온 컬렉션입니다. 하지만 객체 리터럴은 여전히 많은 상황에서 컬렉션으로 활용 될 수 있습니다. 새로운 ES6 컬렉션들이 필요한 상황이 아니라면 굳이 새로운 컬렉션으로 교체할 필요는 없습니다.