웹브라우저 이벤트 다루기

웹브라우저 이벤트 다루기

Contents

  • Event Interface and addEventListener
  • Remove an Event Listener
  • Event Phases
  • Event Object
  • Event Delegation
  • 이벤트 위임 중 Node Type 확인하기

다른 포스트에서 Node Interface와 Element Interface에 대해서 간단히 살펴봤습니다.

Interface 관련 포스트 Link

Event Interface and addEventListener

다른 포스트에서 언급했던 Node Interface와 Element Interface가 모두 상속을 받는 Interface가 있습니다. 바로 Event Target Interface입니다. Event Target은 어떠한 Interface로부터 상속을 받지 않고, chain의 맨 상위에 있습니다.

interface-chain

하지만, 다른 모든 Interface는 Event Target Interface로부터 상속을 받기 때문에, 이것의 속성과 메서드를 포함하고 있습니다. 바꿔 말하면, document object, 웹페이지의 개별 element는 Event Target Interface로부터 상속받습니다.

Event Target은 속성은 없으며, 아래의 세 개의 메서드만 있습니다.

  • eventTarget.addEventListener( );
  • eventTarget.removeEventListener( );
  • eventTarget.dispatchEvent( );

MDN Event Target LInk

Event Target에 event를 추가하기 위해서는 addEventListener를 사용할 수 있습니다.

addEventListener( )

  • Syntax : eventTarget.addEventListener(type, listener[, useCapture]);
  • event target은 웹페이지 상 모든 요소가 event target이 될 수 있습니다.
  • eveny type의 종류는 'click', 'keypress'를 위시해 매우 많은 종류가 있습니다.

Event Reference MDN Link

  • listener는 event가 발생 시 실행할 함수입니다.
  • useCapture의 값으로 boolean을 추가할 수 있습니다. (아래 Event Phase에서 추가 설명)

Remove an Event Listener

addEventListener의 세 번째 parameter를 이용하면, event listener가 어떻게 작동할지 선택을 할 수 있습니다. 하지만 이 기능은 아직 모든 브라우저가 support 하지는 않습니다. 우리는 또 다른 방법인 removeEventListener 메소드를 사용할 수 있습니다.

removeEventListener를 사용하려면, 삭제하려는 동일한 함수를 argument로 전달해야 합니다. 외관상 동일해 보이는 함수가 아닌, 참조 값이 같은 함수를 전달해야 합니다.

function log() {
  console.log('I will keep logging');
}

document.addEventListener('click', log);
document.removeEventListener('click', log);
// 동일한 함수를 전달했기에, 정상적으로 event가 삭제됨.

반면, 아래와 같은 코드는 각자 고유의 listener function을 가지고 있습니다. 따라서 정삭적으로 이벤트 삭제가 되지 않습니다.

document.addEventListener('click', function() {
  console.log('I will keep logging');
});

document.removeEventListener('click', function() {
  console.log('I will keep logging');
});

Event Phases

Event Phase에는 세 단계가 있습니다.

  • capturing phase
  • at target phase
  • bubbling phase

첫 단계인 Capturing phase는 이벤트가 발생했을 때, 타겟이 된 요소까지 부모 요소에서 이벤트가 내려가는 방식입니다.

대부분의 event handler는 at target phase에서 작동합니다. 특정 button에 리스너를 추가하고, event가 발생했을 때 그 event를 위한 handler가 타겟에 함께 위치해 있어 곧바로 작동하게 됩니다.

하지만, 때때로 우리는 여러 아이템의 리스트가 있고, 하나의 event로 모든 아이템을 다루고 싶을때가 있습니다. 기본적으로 특정 아이템을 클릭했을때, handler가 클릭 이벤트를 가로막지 않으면 이벤트는 부모 요소로 타고 오릅니다. (event bubbling)

event

세 단게 중 어떠한 단계에서 addEventListener 메소드가 사용될까요? 그리고 사용되는 단계를 어떻게 바꿀 수 있을까요?

위 사항을 해결하기 위해 addEventListener에 세 번째 argument를 전달해 사용할 수 있습니다. 기본적으로 첫 두개의 arguments만 전달하면, addEventListener 기본 설정인 bubbling phase가 적용됩니다.

하지만, 만약 세번째 argument[useCapture]로 true를 전달하면, capturing phase에서 listener를 실행하게 합니다. Boolean값을 true로 주면, capturing phase에서 event가 해당 요소의 DOM tree 아래에 위치한 다른 target element에 닿기 전, 등록한 listener에 전달되어 listener가 실행됩니다. Event가 target element에 닿은 후 bubbling 단계에서, useCapture가 true인 listener는 실행되지 않습니다.

Event Object

이벤트가 일단 발생하면, 브라우저는 해당 이벤트와 관련된 정보를 담고있는 event object를 포함하게 됩니다. 그리고 이 object를 listener 함수에 전달해 여러 용도로 사용할 수 있습니다.

여러 용도로 사용할 수 있지만, 이벤트 객체를 사용하는 용도 중 많이 사용되고 있는 한 방법은 default action이 실행되지 않게 하는 것입니다.

예를 들어, html의 achor link를 클릭하면 link의 href의 주소로 페이지가 이동하게 됩니다. form의 버튼을 누르면 data를 전송하게 됩니다. 정보를 전송 전 승인하기 위해서, 또는 어떠한 이유로 이러한 과정을 막고 싶을 때 preventDefault( ) 메서드를 사용할 수 있습니다.

const links = document.querySelectorAll('a');
const thirdLink = links[2];

thirdLink.addEventListener('click', function(event) {
  event.preventDefault();
  console.log("Look, ma! We didn't navigate to a new page!");
});

Event Listener 효율적으로 사용하기

아래의 코드를 보면 'div' 요소 안에 200개의 paragraph가 있고, 각 paragraph는 개별적인 event listener를 가지고 있습니다.

const myCustomDiv = document.createElement('div');

for (let i = 1; i <= 200; i++) {
  const newElement = document.createElement('p');
  newElement.textContent = 'This is paragraphs number ' + i;
  newElement.addEventListener('click', function respondToTheClick(evt) {
    console.log('A paragraph was clicked.');
  });

  myCustomDiv.appendChild(newElement);
}

document.body.appenChild(myCustomDiv);

working-with-browser-events-js-the-dom

Refactoring event listeners

위의 코드는 loop가 돌 때마가 동일한 기능을 하는 각각의 함수를 생성하고 있기에, 해당 함수를 별도의 하나의 함수로 만들어 놓고 참조값을 넣을 수 있습니다. 또한, 각 paragraph 마다 event listener를 추가하는 대신 부모 요소인 'div'에 event listener를 추가할 수 있습니다.

const myCustomDiv = document.createElement('div');

function respondToTheClick(evt) {
  console.log('A paragraph was clicked.');
}

for (let i = 1; i <= 200; i++) {
  const newElement = document.createElement('p');
  newElement.textContent = 'This is paragraphs number ' + i;

  myCustomDiv.appendChild(newElement);
}

myCustomDiv.addEventListener('click', respondToTheClick);

document.body.appendChild(myCustomDiv);

working-with-browser-events-js-the-dom-2

위와 같이 코드를 줄일 수 있습니다. 하지만, 부모요소에 listener를 추가했기 때문에, 개별적인 paragraph를 타겟으로 할 수는 없습니다. 어떻게 하면 효율적인 코드를 쓰면서도, 개별적인 paragraph에 접근을 할 수 있을까요?

우리는 event delegation을 이용할 수 있습니다.

Event Delegation

이벤트 위임을 사용하기 위해서 앞서 언급한 Event Object의 속성을 살펴봐야 합니다. Event Object의 target 속성에는 target이 된 요소의 정보가 담겨 있습니다. 위 코드의 paragraph를 click 했을 때의 과정을 살펴보며, event.target에 대해 좀 더 살펴보겠습니다.

  1. paragraph 클릭 이벤트가 발생함.
  2. Event capturing 단계를 거침.
  3. Event가 target 요소에 도착함.
  4. Event Bubbling 단계로 바뀌고 DOM tree를 다시 타고 올라감.
  5. 'div' 요소에 도착하게 되면 listener function이 실행됨.
  6. listener function 내에서 evt.target 속성으로 타겟 요소가 클릭된 것을 확인할 수 있음.

즉, event.target은 타겟이 된 paragraph 요소에 직접 접근을 허락하기 때문에, 우리는 타겟팅된 요소의 text content 수정, style 수정, class 변경 등의 작업을 할 수 있습니다.

const myCustomDiv = document.createElement('div');

function respondToTheClick(evt) {
  console.log('A paragraph was clicked: ' + evt.target.textContent);
}

for (let i = 1; i <= 200; i++) {
  const newElement = document.createElement('p');
  newElement.textContent = 'This is paragraphs number ' + i;

  myCustomDiv.appendChild(newElement);
}

myCustomDiv.addEventListener('click', respondToTheClick);

document.body.appendChild(myCustomDiv);

이벤트 위임 중 Node Type 확인하기

부모 요소에 'click' 이벤트 리스너를 추가하고, 자식 요소를 click 하면 리스너가 실행됩니다. 자식요소에는 'div', 'p', 'span' 등 여러 요소가 있을 경우, 특정 요소를 click 했을 때만 리스터가 실행을 하게 하려면 어떤 방법이 있을까요?

다른 포스트에서 다루었듯이, 모든 요소는 Node Interface로부터 상속을 받습니다. 그 중 nodeName 이라는 속성을 이용해 target 요소의 node 명을 확인할 수 있습니다. 'span'을 클릭하면 'SPAN'을, 'p'를 클릭하면 'P'를 nodeName을 통해 확인할 수 있습니다. 이를 활용하면 아래와 같이 코드를 작성할 수 있습니다.

document.querySelector('#content').addEventListener('click', function(evt) {
  if (evt.target.nodeName === 'SPAN') {
    console.log('A span was clicked with text ' + evt.target.textContent);
  }
});

nodeName의 값은 대문자로 돼 있으니, 코드 작성 시 유의할 필요가 있습니다.