Event

다양한 이벤트는 클릭이 일어난 순간 가장 처음 DOCUMENT 객체에서 이벤트 객체가 생성됩니다. 그리고 캡쳐링 단계가 발생합니다.

이후 순서대로 실제 클릭이 일어난 DOM element까지 이벤트가 전달되며 마지막에는 target으로 이벤트에 접근할수 있게 되는 target 단계가 됩니다. 여기서 target으로 이벤트를 처리한 다음 다시 이벤트가 지나왔던 역순으로 되돌아가는 버블링 단계로 넘어갑니다.

모든 이벤트가 위와같이 일어나지는 않습니다. 일부 이벤트는 이벤트의 대상 엘레멘트에서 바로 생성되어 처리되기도 합니다만, 대부분의 이벤트는 위와 같은 순서를 거칩니다. 이러한 과정을 이벤트가 전파 된다고 이야기 하는 것입니다.

<nav class="nav">
        <img
          src="img/logo.png"
          alt="Bankist logo"
          class="nav__logo"
          id="logo"
        />
        <ul class="nav__links">
          <li class="nav__item">
            <a class="nav__link" href="#section--1">Features</a>
          </li>
          <li class="nav__item">
            <a class="nav__link" href="#section--2">Operations</a>
          </li>
          <li class="nav__item">
            <a class="nav__link" href="#section--3">Testimonials</a>
          </li>
          <li class="nav__item">
            <a class="nav__link nav__link--btn btn--show-modal" href="#"
              >Open account</a
            >
          </li>
        </ul>
      </nav>
const randomNumber = (min, max) => {
  return Math.trunc(Math.random() * (max - min + 1));
};
const randomColor = () =>
  `rgb(${randomNumber(0, 255)},${randomNumber(0, 255)},${randomNumber(
    0,
    255
  )})`;

document.querySelector('.nav__link').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Link', e.target);
});

document.querySelector('.nav__links').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Container', e.target);
});
document.querySelector('.nav').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Nav', e.target);
});
/*
Link <a class="nav__link" href="#section--1" style="background: rgb(232, 147, 108);">Features</a>
Container <a class="nav__link" href="#section--1" style="background: rgb(232, 147, 108);">Features</a>
Nav <a class="nav__link" href="#section--1" style="background: rgb(232, 147, 108);">Features</a>
*/

위 코드를 보면 이벤트로 인해 각각의 이벤트리스너의 콜백에서 this 키워드로 받은 각기 자신의 element들의 색은 모두 랜덤으로 바뀌었습니다. 하지만 각각의 이벤트 리스너의 이벤트는 모두 다른 이벤트 였을까 ?

그건 아닙니다. 이벤트 리스너의 이벤트는 모두 link 에서 시작된 이벤트 입니다.

처음 이벤트는 Document 객체에서 생성되었고 캡쳐링 과정을 거쳐 모든 조상 노드를 거쳐 link element까지 도착했습니다. 여기서 타겟 단계로 이벤트에 대한 조작을 한다음 다시 Document까지 이벤트 버블링 단계를 거쳐 갔습니다.

따라서 link element의 조상 노드들은 모두 Link로부터 시작된 이벤트를 이어 받은 것입니다.

event Handler

addListener, on<event> 등 이있습니다. 보통 이러한 이벤트 핸들러에서 반환하는 값들은 무시됩니다. 하지만 on<event>에서 false값을 반환하는 것은 예외입니다.

menu.onclick = function(event) {
  if (event.target.nodeName != 'A') return;

  let href = event.target.getAttribute('href');
  alert( href ); // 서버에서 데이터를 읽어오거나, UI를 새로 만든다거나 하는 등의 작업이 여기에 들어갑니다.

  return false; // 브라우저 동작을 취소합니다(URL로 넘어가지 않음).
};

위와 같은 코드에서 이벤트 핸들러는 false를 반환합니다. 이러한 경우에 href값에 따라 url이 변경되지 않고 브라우저의 기본동작이 취소됩니다. 즉, url 변경이 이뤄지지 않습니다.

<input value="focus가 동작합니다," onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="클릭해 주세요.">

위 코드를 실제 html에서 구현해보면 mousedown 이벤트를 발생시키는 attribute인 onmousedown 속성은 이벤트가 false를 반환해버리므로 활브라우저 기본동작이 이뤄지지 않습니다. 따라서 해당 이벤트는 벌어지지 않으나 focus 이벤트를 유발시키는 다른 브라우저 동작들이 작동될 수 있습니다.

addEventListener 'passive' 옵션

passive 옵션은 addEventListener의 option 영역에서 passive입니다. 이옵션은 사전에 브라우저에게 이 이벤트는 preventDefault()를 호출하지 않겠다는 것을 알리는 역할을 합니다. 예를 들자면 모바일기기에서는 touchmove라는 이벤튼 자동으로 scrolling을 발생시킵니다. 바로 추가 이벤트가 작동되는 것이지요. 하지만 이벤트 핸들러를 통해 코드를 수행하면서 scrolling을 작동시키지 말아야한다. 라는 preventDefault()코드가 발견이 되면 그제야 브라우저는 이미 실행된 scrolling 이벤트를 취소합니다.

이러한 시간적차이로 브라우저에서는 화면의 떨림이 발생하게 됩니다. 이러한 상황을 막고자 passive옵션을 true로 넣어주면 애초에 브라우저에서 이 이벤트 핸들러는 스크롤링을 취소 하지않을 것이라는 정보를 받게 됩니다. 이로서 사용자는 부드러운 사용감을 느낄 수 있습니다.

addEventListenr 'capture' 옵션

위 와 마찬가지고 capture는 addEventListener의 옵션 객체의 프로퍼티입니다. 하지만 위 passive 옵션과는 다르게 capture 옵션은 축약형이 있습니다.

addEventListener('event', callBackFunc, boolean)
addEventListener('event', callBackFunc, {capture : boolean})
// 위 두개의 코드는 같습니다.

capture : false : default 값으로서 버블링단계에서 이벤트 핸들러가 작동됩니다.

capture : true : 캡쳐링 단계에서 이벤트 핸들러가 작동됩니다.

이렇게 이벤트 리스너의 작동 영역을 다르게 만들었다면 removeEventListener를 사용할 때에도 해당영역에서 삭제하도록 해야합니다.

addEventListener('event', callBackFunc, boolean)
removeEventListener('event', callBackFunc, boolean)

❗️ addEventListener의 세번째 매개변수는 boolean 입니다만 기본값으로false입니다. true로 설정한다면 이벤트가 해당 엘레멘트의 캡쳐 단계를 다룹니다. 캡쳐단계에서부터 해당 이벤트를 다루게 됩니다.

currentTarget

위와 같이 하나의 이벤트를 공유하지 않고 각 요소별로 이벤트를 처리하고 싶다면 currentTarget 프로퍼티를 사용하면 됩니다.

const randomNumber = (min, max) => {
  return Math.trunc(Math.random() * (max - min + 1));
};
const randomColor = () =>
  `rgb(${randomNumber(0, 255)},${randomNumber(0, 255)},${randomNumber(
    0,
    255
  )})`;

document.querySelector('.nav__link').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Link', e.target);
  console.log('Link', e.currentTarget);
});

document.querySelector('.nav__links').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Container', e.target);
  console.log('Container', e.currentTarget);
});
document.querySelector('.nav').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Nav', e.target);
  console.log('Nav', e.currentTarget);
});


이전 코드에서 각 요소에 e.currentTarget 출력을 더한 코드입니다.
결과 코드를 보면 e.target과는 다르게 이벤트가 위치한 현재 엘레멘트를 반환하는 것을 볼 수 있습니다.

e.stopPropagation()

이렇게 전파되어 버블링되는 것을 막을 수 있습니다.

const randomNumber = (min, max) => {
  return Math.trunc(Math.random() * (max - min + 1));
};
const randomColor = () =>
  `rgb(${randomNumber(0, 255)},${randomNumber(0, 255)},${randomNumber(
    0,
    255
  )})`;

document.querySelector('.nav__link').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Link', e.target);
  console.log('Link', e.currentTarget);
  💡 e.stopPropagation();
});

document.querySelector('.nav__links').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Container', e.target);
  console.log('Container', e.currentTarget);
});
document.querySelector('.nav').addEventListener('click', function (e) {
  this.style.background = randomColor();
  console.log('Nav', e.target);
  console.log('Nav', e.currentTarget);
});

❗️ Link element에서 이벤트 버블링이 멈춰서 상위 조상 노드까지 이벤트가 전달되지 못하는 모습입니다. 이러한 과정은 보통 좋지 않습니다. stopProgagation으로 막아놓은 영역은 일종의 Dead 존이 됩니다. 즉, 나중에 사내에서 사용자들의 클릭이나 사용하는 이벤트를 분석하기 위해서 분석 시스템을 적용할 때에 이런식으로 만들어진 DeadZONE은 분석이 되지 않으며 죽은 영역으로 남겨지기 때문에 코드 분석이 어려워 집니다.

💡 event.preventDefault()를 사용하면event.defaultPrevented 속성이 true로 바뀝니다. 이를 이용해서 현재 이벤트의 전파가 막혔는지 막히지 않았는지 알수 있습니다. stopProgagation을 피하는 방법입니다.

이벤트 위임

위와 같이 캡쳐링과 버블링을 이용해서 이벤트 위임이라는 것을 구현할 수 있습니다. 이벤트 위임이라는 것은 어떠한 element 객체 하나에 이벤트 핸들러를 할당해서 해당 객체의 후손 element에서 이벤트가 발생했을 때 이벤트핸들러의 event.target을 이용해서 이벤트가 발생한 위치를 정확하게 핸들링 할 수 있습니다.

document.querySelectorAll('.nav__link').forEach(function (elem) {
  elem.addEventListener('click', function (e) {
    const id = this.getAttribute('href');
    document.querySelector(id).scrollIntoView({ behavior: 'smooth' });
  });
});


document.querySelector('.nav__links').addEventListener('click', function (e) {
  e.preventDefault();
  if (e.target.classList.contains('nav__link')) {
    const id = e.target.getAttribute('href');
    console.log(id);
    document.querySelector(id).scrollIntoView({ behavior: 'smooth' });
  }
});

위 두개의 코드는 같은 기능을 수행하지만 코드를 보게되면 children이 200개 이상 엄청 많게 된다면 어떻게 될까? forEach를 사용하게되면 각각에 addEventListener를 넣게된다. 사용자 입장에서는 느려지는 것을 달성한다. 하지만 이벤트 위임을 사용하면 이벤트가 발생하는 element에 바로 동작하기 때문에 이러한 지연을 줄일 수 있다.

data-action 속성

어떠한 element객체에 이벤트 발생시 작동할 메소드를 만들어 놓았다면 이런 객체와 메소드를 어떻게 연결할 수 있을까요?

바로 위의 이벤트핸들러가 있을수 있지만 더 우아한 방법이 있습니다.
data-action 이라는 속성입니다.

버튼을 이용해서 메소드를 연결해봅니다.

<div id="menu">
  <button data-action="save">저장하기</button>
  <button data-action="load">불러오기</button>
  <button data-action="search">검색하기</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('저장하기');
    }

    load() {
      alert('불러오기');
    }

    search() {
      alert('검색하기');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    <};
  }

  new Menu(menu);
</script>

위코드는 data-action 속성을 통해서 메소드의 이름을 받으면 해당 메소드를 실행하는 클래스 구조입니다.

profile
일상을 기록하는 삶을 사는 개발자 ✒️ #front_end 💻

0개의 댓글