개요
DOM이벤트는 개별 엘리먼트에 걸어줄 수도 있지만 부모 쪽에 걸고 버블링이나 캡쳐링을 이용하는 방법도 있습니다.
우선 자식 엘리먼트가 자주 생성되고 삭제되는 상황이라면 일일히 이벤트를 거는 작업도 피곤할 뿐 아니라 GC문제도 발생하기 때문에 상대적으로 이슈가 적은 부모쪽에 한 번만 걸고 델리게이션을 통해서 자식 이벤트를 처리하려는 거죠.
그 전까지는 다양한 방법은 엘리먼트에 마킹했는데 WeakMap이 나오면서 더 이상 추가적인 마킹없이 델리게이션을 구현할 수 있습니다.
자료구조
우선 간단한 HTML을 생각해보죠.
<div id="wrapper"> <div>wrapper</div> </div> <div> <div id="self">self</div> </div>
1. wrapper의 경우 부모 쪽에 이벤트를 걸려하고
2. self는 본인에게 걸 생각입니다.
WeapMap에 해당 엘리먼트를 베이스로 하는 리스너 저장소를 생각해봅시다.
- 엘리먼트를 키로 하여 다시 Map을 값으로 넣고
- 그 Map에 이벤트 이름별로 Set을 넣는다.
결국 엘리먼트에는 이벤트별로 리스너가 들어가기 때문에 2단 구조가 필요한 셈입니다.
이렇게 만들어지는 WeakMap은 싱글톤이면 충분하니 익명 클래스를 사용해도 충분합니다.
//이벤트리스너를 관리하는 구조체 const ev = new (class extends WeakMap{ constructor(){super();} //리스너추가 - 엘리먼트, 이벤트명, 리스너 addEv(el, event, listener){ //1. 엘리먼트별 Map생성 if(!this.has(el)) this.set(el, new Map); const channel = this.get(el); //2. 이벤트별 Set생성 if(!channel.has(event)) channel.set(event, new Set); //3. 리스너는 이벤트별 Set에 추가 channel.get(event).add(listener); return this; } //이벤트별 Set얻기 getEv(el, event){ if(!this.has(el) || !this.get(el).has(event)) return; return this.get(el).get(event); } });
간단히 2단 자료 구조로 자바식으로 표현하자면 Map<element, Map<eventName, Set
통합 리스너
이제 위에서 작성한 listeners를 검색하여 실제 리스너를 호출해주는 통합이벤트 처리 리스너를 작성할 차례입니다.
구현은 일반적인 버블링 기준으로 진행합니다.
//통합리스너-버블링으로 검색해가자! const listener =e=>{ let {target, type} = e; do{ //현재 타겟에 리스너가 설정되었다면 const list = ev.getEv(target, type); if(list){ //돌면서 실행! list.forEach(f=>f({target, type})); break; } //아니라면 부모로 이동 }while(target = target.parentNode); };
간단한 구조로 딱히 설명할게 없네요. e.target은 항상 가장 마지막의 자식이 되므로 역으로 추적해가면 됩니다.
이제 이를 body에 걸겠습니다.
'click,mousedown,mouseup,touchstart,touchend'.split(',') .forEach(ev=>document.body.addEventListener(ev, listener));
주요 이벤트만 걸었습니다(몇몇 이벤트는 델리게이션에 적합하지 않습니다)
리스너 등록 및 실제 사용
이제 listeners에 원하는 리스너를 등록하여 사용할 수 있습니다.
//대상의 id를 표시한다 const f =({target:{id}})=>console.log(id); //각각 이벤트를 등록한다. 'wrapper,self'.split(',').forEach( id=>ev.addEv(document.querySelector('#' + id), 'click', f) );
이제 wrapper를 클릭하면 부모인 wrapper쪽에 걸린 이벤트가 작동하고
self를 클릭하면 본인에게 걸린 이벤트가 작동하는 것을 볼 수 있습니다.
결론
WeapMap은 객체를 키로 잡을 수 있기 때문에 객체 자체를 건드리지 않고 다양한 작업을 할 수 있습니다. 특히 Weak참조의 특성 상 키 대상 객체가 사라지면 같이 메모리에서 제거되므로 이를 활용하여 다양한 분야에 사용될 수 있죠. 엘리먼트 하나하나에 이벤트를 걸지 않는 델리게이션 방식에서 특정 리스너가 엘리먼트와 매칭되게 하는 부분에서 굉장히 적합하다 할 수 있습니다.
전체 코드는 아래와 같습니다.
<div id="wrapper"> <div>wrapper</div> </div> <div> <div id="self">self</div> </div>
const ev =(_=>{ const ev = new (class extends WeakMap{ constructor(){super();} addEv(el, event, listener){ if(!this.has(el)) this.set(el, new Map); const channel = this.get(el); if(!channel.has(event)) channel.set(event, new Set); channel.get(event).add(listener); return this; } getEv(el, event){ if(!this.has(el) || !this.get(el).has(event)) return; return this.get(el).get(event); } }); const listener =e=>{ let {target, type} = e; do{ const list = ev.getEv(target, type); if(list){ list.forEach(f=>f({target, type})); break; } }while(target = target.parentNode); }; 'click,mousedown,mouseup,touchstart,touchend' .split(',') .forEach(ev=>document.body.addEventListener(ev, listener)); return ev; })(); const f =({target:{id}})=>console.log(id); 'wrapper,self' .split(',') .map(v=>document.querySelector('#' + v)) .forEach(el=>ev.addEv(el, 'click', f));
recent comment