Javascript Object vs Map 당신의 선택은
들어가기전
Javascript를 배우거나 사용하다보면 Object 외에 Map이라는 것을 접하게 됩니다.
"Object도 key-value로 저장하는데 Map은 뭐가 다르길래 사용하는거지?"
이런 궁금증을 가져보신 적 있나요?
Map은 ES6에서 도입된 강력한 컬렉션으로, 단순히 Object의 대안이 아닌 고유의 특성과 장점을 가진 자료구조입니다.
Map이 도대체 뭐길래
Map 객체는 key-value
형태로 저장하는 컬렉션으로, 키의 타입에 제한이 없고 삽입 순서를 유지하는 해시맵 자료구조입니다.
탄생배경
Map 이전에는 Object밖에 없어 이를 활용했어야 했습니다. 하지만 이 때 여러 문제들을 겪게 됩니다.
- 키 타입 제한
- 프로토타입 오염
- 크기 확인 비효율성
위의 문제들을 해결하기 위해서 Map이 등장하게 됩니다.
Map 메서드
기본적인 메서드입니다
메서드를 통해 객체보다 동작과 의도를 명확하게 보여줍니다.
const obj = new Map(); //Map 생성
// set 맵에 삽입
obj.set('name','홍길동')
obj.set('age',20)
obj.set('phone','010-1234-5678')
// get 맵 조회
obj.get('age')
// has 존재여부 조회 boolean값 반환
obj.has('age')
// delete 삭제
obj.delete('age')
// keys 삽입 순서에 따라 key를 포함한 반복자 반환
obj.keys().map...
// values 삽입 순서에 따라 값을 포함한 반복자 반환
obj.values().map...
// entries, forEach....
// clear 맵 초기화
obj.clear()
Object와 차이
- 프로토타입 키가 없는 깨끗한 상태로 시작합니다.
const obj = {};
const map = new Map();
// Object는 이미 프로토타입 프로퍼티들이 존재
console.log('constructor' in obj); // true (프로토타입에서 상속)
console.log('toString' in obj); // true
console.log(Object.keys(obj)); // []
// Map은 키-값 저장소가 완전히 비어있음
console.log(map.has('constructor')); // false
console.log(map.has('toString')); // false
console.log(map.size); // 0
```
2. 크기 정보를 내장하여 즉시 사이즈를 가져올 수 있습니다.
3. key값은 모든 값을 사용할 수 있습니다.
```js
const map = new Map()
map.set(()=>{},'function')
map.set({id:1},'object')
map.set(1,'number')
map.set('string','string')
map.set(false,'boolean')
...
- 삽입 순서를 보장합니다.
- iterable하기 때문에 직접 반복할 수 있습니다.
const map = new Map();
...
map.forEach(...)
map.entries(...)
- 빈번한 추가 및 제거와 관련된 상황에서는 Object보다 성능이 우세합니다
- 직렬화에 대한 기본 지원이 없습니다.
JSON.stringify에 인자로 replacer를 활용하여 지원하게 만들 수 있습니다
어떤 것을 써야할까
Map을 사용해야하는 곳
- 동적인 키값이 많은 곳
- 빈번한 추가 및 삭제가 있는 곳
- 키의 순서가 중요한 곳
- 프로토타입 오염을 피하고 싶은 곳
Object를 사용해야하는 곳
- 미리 정해진 정적 구조(설정값)
- JSON 호환성이 필요한 곳
WeakMap은 무엇이 다를까
WeakMap은 객체만을 키값으로 사용하며, 약한 참조를 통해서 메모리 누수를 방지하는 Map입니다.
왜 WeakMap일까
특정 key에 대한 값이 있는지는 메서드를 통해서 확인되지만, 키로 보유한 객체들을 열거하는 기능은 제공되지 않습니다. 프로그램 내 객체에 대한 참조가 WeakMap
을 제외하고 존재하지 않다면 해당 객체는 가비지 컬렉션에 등록됩니다.
ToastUI에서 발췌한 글에 좋은 예시가 있어 가져왔습니다.
class EventEmitter {
constructor() {
this.targets = new WeakMap();
}
on(targetObject, handlers) {
if (!this.targets.has(targetObject)) {
this.targets.set(targetObject, {});
}
const targetHandlers = this.targets.get(targetObject);
Object.keys(handlers).forEach((handlerName) => {
targetHandlers[handlerName] = targetHandlers[handlerName] || [];
targetHandlers[handlerName].push(handlers[handlerName].bind(targetObject));
});
}
fire(targetObject, handlerName, args) {
const targetHandlers = this.targets.get(targetObject);
if (targetHandlers && targetHandlers[handlerName]) {
targetHandlers[handlerName].forEach((handler) => handler(args));
}
}
}
const emitter = new EventEmitter();
function start() {
const user = {
name: 'John',
};
const handlers = {
sayHello: function () {
console.log(`Hello, my name is ${this.name}`);
},
sayGoodBye: function () {
console.log(`Good bye, my name is ${this.name}`);
},
};
emitter.on(user, handlers);
const timer = setInterval(() => {
emitter.fire(user, 'sayHello');
emitter.fire(user, 'sayGoodBye');
}, 1000);
setTimeout(function () {
clearInterval(timer);
}, 5001);
}
위에 있는 코드는 ToastUI에서 작성된 코드로 CustomEvent
를 위한 코드입니다. 지금은 WeakMap
으로 작성되어 Map
으로 바꾸어 어떤차이가 있는지 확인할 수 있습니다.
우선 코드를 보면 EventEmitter
인스턴스를 생성하고 start
함수를 호출하면
필요한 객체들을 생성하여 EventEmitter
에 등록하고 setInterval
을 통해 반복적으로 이벤트를 실행합니다.
함수가 종료된 이후에는 user의 객체를 참조하고 있는 것은 없기 때문에 WeakMap에서는 가비지 콜렉터에 등록시켜 수거를 하게 됩니다
