ECMAScript | TypeScript

ECMAScript, 딕셔너리와 Map

partner_jun 2017. 8. 21. 16:19

많은 언어에서 쓰이는 콜렉션 중에는 Map이라는 콜렉션이 있다. Map은 Key-Value 콜렉션으로 특정 키에 대한 값을 가지는 형태다. NoSql이나 In-memory DB에서도 사용되는 아주 유용한 형태다. 아래 코드들은 모두 TypeScript에서 작성했다.



1. Dictionary Object

ES6 전까지의 자바스크립트에는 Map의 구현체가 없었다. 대신 객체 프로퍼티를 자유롭게 추가/삭제할 수 있다는 장점을 이용해 특정 프로퍼티에 값을 할당한 형태인 '딕셔너리(Dictionary)' 객체를 만들어 사용해 왔다.

const dictObj = {};
dictObj['first'] = 1;
dictObj['second'] = 'second';
dictObj[10] = '100'; // key인 10은 string으로 변환된다

console.log(dictObj['10']); // 100

다른 언어에서 흔히 사용하는 Map과 달리 딕셔너리 객체의 Key는 string로 한정된다. 다른 자료형을 넣더라도 string으로 자동 형변환된다.


위 코드에서 만든 dictObj는 단순한 객체로 Key에 해당하는 프로퍼티명, Value에 해당하는 프로퍼티 값을 가지고 있을 뿐이다. 아래 코드로 확인해볼 수 있다.

// 모든 키를 가져오기
let keys: string[] = Object.keys(dictObj);
console.log(keys); // ["100", "first", "second"]

// 모든 프로퍼티를 가져오기
let propertys: string[] = Object.getOwnPropertyNames(dictObj);
console.log(propertys); // ["100", "first", "second"]

dictObj를 직접 출력해도 단순 객체라는 사실을 확인할 수 있다.


또, 일반 객체처럼 for문을 이용해 객체의 프로퍼티를 순회할 수 있다.

// 딕셔너리 객체의 키 순회하기
for(let key in dictObj) {
console.log(key + ' : ' + dictObj[key]);
}
/*
10 : 100
first : 1
second : second
*/




2. Map

ECMAScript6에서 추가된 Map은 prototype에 몇 가지 유용한 메소드가 구현되어 있는 클래스다. 다른 언어들의 Map 콜렉션과 아주 유사하다.

const map = new Map([
['ten', 10]
]); // <type, type>으로 자료형을 지정할 수 있음

map.set('first', 1);
map.set('second', 2);

let ten = map.get('ten');
console.log(ten); // 10

타입 스크립트에서는 Map 객체를 만들 때 다이아몬드 연산자(<>)를 이용해 타입을 제한할 수 있다.


다른 언어에서의 Map과 마찬가지로 Key나 Value의 이터레이터를 얻어올수도 있다.

// key 이터레이터 얻어오기
let keys: IterableIterator<string> = map.keys();
console.log(keys); // MapIterator {"first", "second"}

// key-value pair 이터레이터 얻어오기
let entires = map.entries();
console.log(entires); // MapIterator {"ten" => 10, "first" => 1, "second" => 2}


 이 때 반환되는 객체는 배열이 아닌 MapIterator의 객체다. 때문에 이터레이터의 next 메소드를 이용해 순회해야 한다.

// 이터레이터를 이용해 순회하기
while(true) {
let tuple = entires.next().value;
if(!tuple) break;
console.log(tuple[0] + ' : ' + tuple[1]);
}
/*
first : 1
second : 2
*/


혹은 고차함수 forEach를 이용해 Map을 순회할 수 있다. 하지만 map이나 filter같은 고차함수가 구현되어있지 않은 점은 아쉽다.

// key-value 순회하기
map.forEach((value, key) => {
console.log(value + ' : ' + key);
});
/*
10: ten
1: first
2 : second
*/



ECMAScript의 특성상 Object를 Key로 사용할 때는 고려해야 할 점이 더 있다.


Java에서는 객체를 비교할 때 equals, hashCode 메소드를 이용하는데, 이 메소드들을 오버라이드하여 원하는대로 객체의 동등성/동일성을 재정의할 수 있다. Set이나 Map 등의 콜렉션에서는 이런 메소드들을 이용해 객체를 비교한다. 


하지만 ECMAScript에서는 그런 비교가 불가능하다. 결과적으로, ES6의 Map의 키로 사용된 객체의 '임의적인' 비교가 불가능한 것이다.

class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user1 = new User('Jack');
const user2 = new User('Duck');
const user3 = new User('Jack');
const map = new Map<User, number>([
[user1, 10]
]);
map.set(user2, 20);
map.set(user3, 30);

map.forEach((value, key) => {
console.log(key.name + ' : ' + value);
});
/*
Jack : 10
Duck : 20
Jack : 30
*/

user1 == user2는 false기 때문에 다른 'key'로 인식한다.


이런 경우 key를 JSON 형태로 변환하면 되지만 올바른 해결법이라기에는 아쉬움이 남는다.

const map = new Map<string, number>();
map.set(JSON.stringify(user1), 20);
map.set(JSON.stringify(user3), 30);

map.forEach((value, key) => {
console.log(JSON.parse(key).name + ' : ' + value);
});
/*
Jack : 30
*/



3. Dictionary와 Map, 어느 것을 사용해야 하나?

모질라 제단의 Map 문서에는 Map과 Object의 비교와 선택 가이드가 적혀있다. 내용 중 Map 사용이 더 적합한 경우는 아래와 같다.


  • 통상적으로 키(Key)는 런타임까지 알 수 없으며, 유동적으로 그것들을 찾아야 하는가?
  • 모든 값(Value)는 동일한 타입을 가지고 있으며, 상호 변환이 가능하도록 사용되는가?
  • 문자열이 아닌 키(Key)가 필요한가?
  • 키-값 짝이 때때로 추가되거나 삭제되는가?
  • 자주, 쉽게 변하는 임의의 양의 키-값 짝이 있는가?
  • 데이터가 Iterated 되어야 하는가?