[JS-FINAL] 8. 이벤트

게코젤리·2023년 5월 29일

유데미 강의 JavaScript 완벽 가이드 : 초급 + 고급 마스터 과정을 들으면서 자바스크립트를 다시 한 번 익히는 중이다. 계속되는 포스팅에서 강의를 듣고 미처 몰랐던 부분, 심화 이해가 필요한 부분에 대해 주제별로 간단하게 기록하겠다.

1. 이벤트 리스너

  • removeEventListener를 하기 위해선 이벤트 이름, 함수가 같아야 한다. 익명 함수는 제거할 수 없다.
const onClickBtn = () => {};
btn.addEventListener('click', onClickBtn);
btn.removeEventListener('click', onClickBtn);
  • bind() 메서드를 사용할 경우 새로운 함수 객체를 생성해 서로 다른 참조를 갖기 때문에 removeEventListener로 제거할 수 없다. 이를 해결하려면 bind 메소드를 호출한 결과를 변수에 저장하고, 그 변수를 사용하여 이벤트 리스너를 등록 및 제거해야 한다.
// remove되지 않는다.
btn.addEventListener('click', onClickBtn.bind(this));
btn.removeEventListener('click', onClickBtn.bind(this));
// 해결
const boundFn = onClickBtn.bind(this);
btn.addEventListener('click', boundFn);
btn.removeEventListener('click', boundFn);

2. 이벤트 캡처링, 버블링

  1. 캡처링 단계: 최상위 요소(보통 document 혹은 window 객체)에서 시작하여 실제 이벤트가 발생한 요소까지 내려가면서 이벤트가 발생한 요소를 추적하는 단계. 이 단계에서 이벤트 리스너가 동작하게 하려면, addEventListener의 세 번째 인자를 true로 설정해야 한다.

  2. 타깃 단계 : 이벤트가 실제로 발생한 요소에 도달하면 해당 요소에 등록된 이벤트 리스너가 동작한다.

  3. 버블링 단계: 이벤트는 발생한 요소에서 다시 최상위 요소로 '올라간다'. 이 단계에서 이벤트 리스너가 동작하게 하려면, addEventListener의 세 번째 인자를 false로 설정하거나 생략한다.

3. 이벤트 전파 막기

  • event.stopPropagation()를 통해 이벤트 전파를 막을 수 있다. (캡처링 방향, 버블링 방향 둘 다)
  • event.stopImmediatePropagation() 이벤트의 전파를 중지시키는 동시에 현재 요소에 등록된 나머지 이벤트 핸들러들의 실행도 중지.
btn.addEventListener('click', () => {
  event.stopImmediatePropagation();
})
btn.addEventListener('click', handler1) // 실행안됨
btn.addEventListener('click', handler2) // 실행안됨
  • mouseenter 이벤트는 DOM 트리에서 버블링되지 않는다. 이는 mouseenter 이벤트가 특정 요소에 진입했을 때만 그 요소에서 발생하며, 부모 요소로 전파되지 않는다.

4. 이벤트 위임

상위 요소에 이벤트 리스너를 달아 하위 요소에서 발생하는 이벤트를 감지하는 것.

  • 메모리 사용량을 줄일 수 있다. 모든 하위 요소에 이벤트 핸들러를 추가하는 대신 상위 요소에 단 하나의 이벤트 핸들러만 추가하면 된다.
  • 동적 요소 처리가 가능하다. 새로운 요소가 추가되더라도 별도의 이벤트 핸들러를 추가하지 않아도 된다.
// 나이브한 이벤트리스너 사용
const listItems = document.querySelectorAll('li');
listItems.forEach(item => {
  item.addEventListener('click', e => {
    e.target.classList.toggle('hightlight')
  })
})

const list = document.querySelector('ul');
list.addEventListener('click', e => {
  // currentTarget은 이벤트 리스너가 부착된 요소
  // e.target은 클릭되어진 실제 목표를 참조, 단 이벤트 위임은 복잡한 구조에서는 문제가 생길 수 있음
  // e.target.classList.toggle('hightlight')
  
  // 완벽한 이벤트 위임 패턴, 이벤트를 위임하되 DOM 탐색을 조합
  e.target.closest('li').classList.toggle('hightlight');
})

5. 드래그앤드롭 기능

// html 설정
<li id="p3" draggable="true">

connectDrag() {
    const item = document.getElementById(this.id);
    item.addEventListener('dragstart', (e) => {
      // 드롭시 (dragenter, dragover) 'text/plain'으로 설정한 데이터만 받을 수 있도록
      e.dataTransfer.setData('text/plain', this.id);
      e.dataTransfer.effectAllowed = 'move';

      const clone = item.cloneNode(true);
      clone.style.position = 'absolute';
      clone.style.width = '300px';
      clone.style.top = '-9999px';
      document.body.appendChild(clone);

      e.dataTransfer.setDragImage(clone, 0, 0);
      setTimeout(() => document.body.removeChild(clone), 0);
    });
  }  
  
connectDroppable() {
    const listEl = document.querySelector(`#${this.type}-projects ul`);
    // dragenter : 드래그 중인 요소가 드롭 대상 요소의 범위로 처음 진입할 때 한 번만 발생
    // dragover : 드래그 중인 요소가 드롭 대상 요소의 범위 위에서 마우스를 움직일 때마다 계속해서 발생
    listEl.addEventListener('dragenter', (e) => {
      if (e.dataTransfer.types[0] === 'text/plain') {
        listEl.parentElement.classList.add('droppable');
        e.preventDefault();
      }
    });
    listEl.addEventListener('dragover', (e) => {
      // console.log(e.dataTransfer);
      if (e.dataTransfer.types[0] === 'text/plain') {
        e.preventDefault();
      }
    });

    listEl.addEventListener('dragleave', (e) => {
      if (e.relatedTarget.closest(`#${this.type}-projects ul`) !== listEl) {
        listEl.parentElement.classList.remove('droppable');
      }
    });
    listEl.addEventListener('drop', (e) => {
      const dropElId = e.dataTransfer.getData('text/plain');
      if (this.projects.find((p) => p.id === dropElId)) {
        return;
      }

      document
        .getElementById(dropElId)
        .querySelector('button:last-of-type')
        .click();
    });
    listEl.addEventListener('dragend', (e) => {
      listEl.parentElement.classList.remove('droppable');
      // e.preventDefault(); // not required
    });
  }  

0개의 댓글