본문 바로가기
언어 Language/자바스크립트 JavaScript

[JavaScript] 이벤트 (2) - 다른 이벤트 개념들 (이벤트 객체, 이벤트 전파(버블링, 캡처링), 이벤트 위임)

by 이땡칠 2022. 6. 15.

 

요약

1. 이벤트 타겟
2. 이벤트 타입
3. 이벤트 객체
4. 이벤트 핸들러
5. 이벤트 전파
6. 이벤트 위임

 

이벤트 타겟 (event target) 

이벤트가 일어날 객체를 의미한다. 

 

이벤트 타입 (event type)

이벤트의 종류를 의미한다.

이벤트의 종류는 이미 약속되어 있다

 

 

이벤트 객체

이벤트 핸들러 함수 내부에서, 여러분은 event, evt, 혹은 e와 같은 이름으로 명명된 매개변수(parameter)를 봤을 것입니다. 이것들은 이벤트 객체라고 불리고, 추가적인 기능과 정보를 제공하기 위해 이벤트 핸들러에 자동으로 전달됩니다.

 

예제

function bgChange(e) {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener('click', bgChange);

 

여기서 여러분은 우리가 이벤트 객체, e를, 함수에 포함하고, 함수에서 배경 색상 스타일을 — 버튼 그 자체인 — e.target에서 설정한 것을 볼 수 있습니다. 이벤트 객체의 target 프로퍼티는 항상 이벤트가 발생된 요소에 대한 참조입니다. 그래서, 이 예제에서, 우리는 무작위의 배경색을 페이지가 아니라, 버튼에 설정했습니다.

 

참고

이벤트 객체에 대해 여러분이 좋아하는 어떠한 이름이든 사용할 수 있습니다.
여러분은 단지 이벤트 핸들러 함수 내에서 그것을 참조하기 위해 사용할 수 있는 이름을 선택할 필요가 있습니다. 

e / event 가 가장 일반적으로 개발자들에 의해 사용됩니다. 왜냐하면 짧고 기억하기 쉽기 때문입니다.
일관적인 것은 항상 좋습니다

 

같은 이벤트 핸들러를 다수의 요소에 설정하고 그것들에 이벤트가 발생되었을 때 그것들 모두에 뭔가를 하기를 원할 때 e.target은 엄청나게 유용합니다. 예를 들자면, 여러분에게 선택되었을 때 사라지는 16개의 타일 세트가 있다고 합시다. 타일을 몇몇 더욱 어려운 방법으로 선택해야만 하는 것 대신에, e.target으로서 단지 타일을 사라지게 항상 설정할 수 있는 것은 유용합니다. 

 

다음의 예제에서 우리는 16개의 <div> 요소를 JavaScript를 통해 생성했습니다. 우리는 그리고서 document.querySelectorAll()을 사용해 그것들 모두를 선택했고, 그리고서 선택되었을 때 무작위 색상이 적용되도록 만드는 onclick 핸들러를 각각에 추가하며 요소들을 순회했습니다:

 

const divs = document.querySelectorAll('div');

for (let i = 0; i < divs.length; i++) {
  divs[i].onclick = function(e) {
    e.target.style.backgroundColor = bgChange();
  }
}

 

여러분이 만날 대부분의 이벤트 핸들러들은 이벤트 객체에서 사용 가능한 표준 프로퍼티와 함수 (메서드) 집합을 가지고 있습니다; 전체 리스트를 위해 Event 객체 레퍼런스를 참조해 보세요. 그러나 몇몇의 더욱 고급 핸들러들은 그들이 기능하기를 필요로 하는 추가적인 데이터를 포함하는 전문적인 프로퍼티들을 추가합니다. 예를 들어, Media Recorder API dataavailable 이벤트를 가지고 있는데, 이는 몇몇 오디오나 비디오가 기록되고 뭔가를 할 수 있을 때 (예를 들자면 저장하거나, 다시 재생하거나) 발생됩니다. 해당하는 ondataavailable 핸들러의 이벤트 객체는 여러분이 그것에 접근하거나 그것으로 무언가를 할 수 있게 하는 녹화된 오디오나 비디오 데이터를 포함하는 이용 가능한 data 프로퍼티를 가지고 있습니다.

 

참고

프로젝트에서 오디오 저장, 재생 등 작업을 하려고 한다면 위 내용은 잘 봐둘 필요가 있을 것 같습니다!

 

 

기본 행동 방지하기

때때로, 이벤트가 기본으로 하는 것을 방지하고 싶은 상황에 마주칠 수 있습니다. 가장 일반적인 예제는 웹 양식에 관한 것인데, 예를 들자면, 커스텀 등록 양식입니다. 세부 사항을 채우고 제출 버튼을 선택했을 때, 자연적인 행동은 데이터가 처리를 위해 서버에 있는 명시된 페이지로 제출되는 것이고, 브라우저는 몇몇 종류의 "성공 메시지" 페이지로 리다이렉트되는 것입니다 (혹은 만약 다른 것이 명시되지 않았다면, 같은 페이지로).

문제는 유저가 데이터를 옳게 제출하지 않았을 때 발생합니다 — 개발자로서, 여러분은 서버로의 제출을 방지하고 무엇이 잘못되었고 옳게 되기 위해 무엇이 완료되어야 하는지를 말하는 에러 메시지를 주기를 원합니다. 몇몇 브라우저는 자동 양식 데이터 확인 기능을 제공하지만, 많은 브라우저들은 그렇지 않으므로, 그것들에 의존하지 않고 여러분만의 점검 기능을 구현하는 것이 낫습니다. 간단한 예제를 살펴봅시다.

 

우선, 이름과 성을 입력하기를 요구하는 간단한 HTML 양식입니다.

<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text">
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text">
  </div>
  <div>
     <input id="submit" type="submit">
  </div>
</form>
<p></p>
 

이제 JavaScript입니다 — 여기 우리는 텍스트 필드가 비었는지를 검사하는 onsubmit 이벤트 핸들러 (제출 이벤트는 양식이 제출되었을 때 발생됩니다) 내부에 아주 간단한 점검을 구현했습니다. 만약 텍스트 필드가 비었다면, 우리는 이벤트 객체에 있는 — 양식 제출을 멈추는 — preventDefault() 함수를 호출하고 그리고서 유저에게 무엇이 잘못되었는지를 말하기 위해 양식 아래에 있는 단락에 에러 메시지를 표시합니다.

const form = document.querySelector('form');
const fname = document.getElementById('fname');
const lname = document.getElementById('lname');
const para = document.querySelector('p');

form.onsubmit = function(e) {
  if (fname.value === '' || lname.value === '') {
    e.preventDefault();
    para.textContent = 'You need to fill in both names!';
  }
}
 

 

 

이벤트 전파 (이벤트 버블링, 캡처링)

이벤트 버블링과 캡처는 같은 이벤트 타입의 두 이벤트 핸들러가 한 요소에서 작동되었을 때 무슨 일이 일어나는지를 기술하는 두 메커니즘입니다. 

 

이것은 <video>를 내부에 가지고 있는 <div>를 보이고 감추는 아주 간단한 예제입니다.

<button>Display video</button>

<div class="hidden">
  <video>
    <source src="rabbit320.mp4" type="video/mp4">
    <source src="rabbit320.webm" type="video/webm">
    <p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p>
  </video>
</div>


 <script>

      const btn = document.querySelector('button');
      const videoBox = document.querySelector('div');
      
      
      (...)
      
</script>

 

<button>이 선택되었을 때, <div>의 클래스 어트리뷰트를 hidden에서 showing으로 바꿈으로써, 비디오는 표시됩니다. (예제의 CSS는 이 두 클래스를 포함하고 있는데, 각각 박스를 화면에서 벗어나게 만들고 들어오게 위치시킵니다):

btn.onclick = function() {
  videoBox.setAttribute('class', 'showing');
}

 

우리는 그리고서 두 개의 onclick 이벤트 핸들러를 추가합니다 — 첫번째는 <div>에 대한 것이고 두번째는 <video>에 대한 것입니다. 이제, 비디오 외부의 <div> 영역이 선택되었을 때, 박스는 다시 숨겨져야만 하고 비디오 그 자체가 선택되었을 때, 비디오는 재생을 시작해야만 합니다.

videoBox.onclick = function() {
  videoBox.setAttribute('class', 'hidden');
};

video.onclick = function() {
  video.play();
};

 

하지만 문제가 있습니다 — 현재, 비디오를 클릭했을 때 이것은 재생을 시작하나, 동시에 <div>가 숨겨지는 것을 유발합니다. 이것은 왜냐하면 비디오가 <div> 내부에 있기 때문입니다 — 비디오는 div의 부분입니다 — 그래서 비디오를 선택하는 것은 실제로는 위의 두 이벤트 핸들러를 다 실행합니다.

 

버블링과 캡처링 설명

부모 요소를 가지고 있는 요소에서 이벤트가 발생되었을 때 (이 경우, <video>는 부모로서의 <div>를 가지고 있습니다), 현대의 브라우저들은 두 가지 다른 단계(phase)를 실행합니다 — 캡처링(capturing) 단계와 버블링(bubbling) 단계입니다.

 

캡처링 단계에서는:

  • 브라우저는 요소의 가장 바깥쪽의 조상 (<html>)이 캡처링 단계에 대해 그것에 등록된 onclick 이벤트 핸들러가 있는지를 확인하기 위해 검사하고, 만약 그렇다면 실행합니다.
  • 그리고서 <html> 내부에 있는 다음 요소로 이동하고 같은 것을 하고, 그리고서 그 다음 요소로 이동하고, 실제로 선택된 요소에 닿을 때까지 계속합니다.

 

버블링 단계에서는, 정확히 반대의 일이 일어납니다:

  • 브라우저는 선택된 요소가 버블링 단계에 대해 그것에 등록된 onclick 이벤트 핸들러를 가지고 있는지 확인하기 위해 검사하고, 만약 그렇다면 실행합니다.
  • 그리고서 그것은 바로 다음의 조상 요소로 이동하고 같은 일을 하고, 그리고서 그 다음 요소로 이동하고, <html> 요소에 닿을 때까지 계속합니다.

현대의 브라우저들은, 기본으로, 모든 이벤트 핸들러들은 버블링 단계에 대해 등록되어 있습니다. 그래서 우리의 현재 예제에서는, 비디오를 선택했을 때, 이벤트는 <video> 요소로부터 밖으로 나가 <html> 요소까지 올라갑니다(bubble). 그 동안 다음이 일어납니다:

 

  • video.onclick... 핸들러를 발견하고 실행하므로, 비디오가 먼저 재생을 시작합니다.
  • 그리고서 videoBox.onclick... 핸들러를 발견하고 실행하므로, 비디오는 또한 숨겨집니다.

 

참고: 버블링과 캡처링, 두 타입의 이벤트 핸들러가 모두 존재하는 경우에, 캡처링 단계가 먼저 실행되고, 이어서 버블링 단계가 실행됩니다.

 

stopPropagation()으로 문제 고치기

이것은 몹시 짜증나는 움직임이지만, 고칠 방법이 있습니다! 표준 Event 객체는 stopPropagation()라 불리는 사용 가능한 함수를 가지고 있는데, 핸들러의 이벤트 객체가 호출되었을 때, 이는 첫번째 핸들러가 실행되지만 이벤트가 더 이상 위로 전파되지 않도록 만들어, 더 이상의 핸들러가 실행되지 않도록 합니다.

그러므로, 이전 코드 블럭에 있는 두 번째 핸들러 함수를 다음으로 변경함으로써 우리는 현재의 문제를 고칠 수 있습니다:

video.onclick = function(e) {
  e.stopPropagation();
  video.play();
};

 

 
참고:
 왜 캡처링과 버블링으로 애를 쓰냐구요? 글쎄요, 브라우저들이 지금보다 훨씬 덜 호환되던 옛날의 좋지 못하던 시절에, Netscape는 오직 이벤트 캡처링만을 사용했고, Internet Explorer는 오직 이벤트 버블링만을 사용했습니다. W3C가 이 움직임을 표준화하고 합의에 이르기를 시도하기로 결정했을 때, 그들은 양 쪽을 다 포함하는 이 시스템을 채용하게 되었는데, 이것이 현대 브라우저들이 구현한 것입니다.
참고:
 위에서 언급했다시피, 기본적으로 모든 이벤트 핸들러는 버블링 단계에 등록되어 있고, 이것은 대부분의 경우 더 타당합니다. 만약 정말로 이벤트를 캡처링 단계에 대신 등록하기를 원한다면, addEventListener()를 사용하고, 옵션인 세 번째 프로퍼티를 true로 설정하여 핸들러를 등록함으로써 그렇게 할 수 있습니다.

 

 

이벤트 위임 (Event Delegation)

버블링은 또한 이벤트 위임의 이점을 취할 수 있게 합니다 — 이 개념은 만약 다수의 자식 요소 중 하나를 선택했을 때 코드를 실행하기를 원한다면, 모든 자식에 개별적으로 이벤트 리스너를 설정해야만 하는 것 대신 이벤트 리스너를 그들의 부모에 설정하고 그들에게서 일어난 이벤트가 그들의 부모에게까지 올라오게 할 수 있다는 사실에 의존합니다. 기억하세요, 버블링은 이벤트 핸들러에 대해 이벤트가 발생된 요소를 먼저 검사하고서, 요소의 부모 등등으로 올라가는 것을 포함합니다.

 

하나의 좋은 예시는 일련의 리스트 아이템들입니다 — 만약 각각이 선택되었을 때 메시지를 띄우기(pop up)를 원한다면, 여러분은 click 이벤트 리스너를 부모 <ul>에 설정할 수 있고, 이벤트들은 리스트 아이템들에서 <ul>까지 올라갈 것입니다.

 

예제1

 

HTML은 대략 다음과 같습니다.

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

 

지금 보는 표에는 9개의 칸이 있습니다. 하지만 칸이 99개이든 9,999개이든 상관없습니다.

 

지금 해야 할 작업은 <td>를 클릭했을 때, 그 칸을 강조하는 것입니다.

 

 <td>마다 onclick 핸들러를 할당하는 대신, ‘모든 이벤트를 잡아내는’ 핸들러를 <table> 요소에 할당해 보겠습니다.

<table> 요소에 할당하게 될 핸들러는 event.target을 이용해 어떤 요소가 클릭 되었는지 감지하고, 해당 칸을 강조하게 됩니다.

 

코드는 아래와 같습니다.

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // 클릭이 어디서 발생했을까요?

  if (target.tagName != 'TD') return; // TD에서 발생한 게 아니라면 아무 작업도 하지 않습니다,

  highlight(target); // 강조 함
};

function highlight(td) {
  if (selectedTd) { // 이미 강조되어있는 칸이 있다면 원상태로 바꿔줌
    selectedTd.classList.remove('highlight'); 
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // 새로운 td를 강조 함
}

// classList 는 엘리먼트의 className 을 읽는 프로퍼티입니다.
// classList.add() 는 기존 class 에 공백을 기준으로 값을 추가합니다.
// classList.remove() 는 class 에 해당 값이 있다면 값을 삭제합니다.

 

 

이렇게 코드를 작성하면 테이블 내 칸의 개수는 고민거리가 되지 않습니다. 강조기능을 유지하면서 <td>를 언제라도 넣고 뺄 수 있게 됩니다.

 

하지만 단점도 있습니다.

위와 같이 구현하면 클릭 이벤트가 <td>가 아닌 <td> 안에서 동작할 수 있습니다.

팔괘도의 HTML을 살펴봅시다. <td>안에 중첩 태그 <strong>이 있는 것을 확인할 수 있습니다.

<td>
  <strong>Northwest</strong>
  ...
</td>

<strong>을 클릭하면 event.target에 <strong>에 해당하는 요소가 저장됩니다.

 

따라서 table.onclick 핸들러에서 event.target을 이용해 클릭 이벤트가 <td>안쪽에서 일어났는지 아닌지를 알아내야 합니다.

이런 단점을 반영하여 기능을 향상한 코드는 아래와 같습니다.

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

설명:

  1. elem.closest(selector) 메서드는 elem의 상위 요소 중 selector와 일치하는 가장 근접한 조상 요소를 반환합니다. 위 코드에선 이벤트가 발생한 요소부터 시작해 위로 올라가며 가장 가까운 <td> 요소를 찾습니다.
  2. event.target이 <td>안에 있지 않으면 그 즉시 null을 반환하므로 아무 작업도 일어나지 않습니다.
  3. 중첩 테이블이 있는 경우 event.target은 현재 테이블 바깥에 있는 <td>가 될 수도 있습니다. 이런 경우를 처리하기 위해 <td>가 팔괘도 안에 있는지를 확인합니다.
  4. 이제 진짜 td를 강조해 줍니다.

이렇게 구현하면 <td>의 개수에 상관없이 원하는 <td>를 강조해주는 코드를 빠르고 효율적으로 구현할 수 있습니다.

 

 

 

참고 

 MDN 이벤트 입문

 MDN 이벤트 레퍼런스

 MDN Event 객체 레퍼런스

JAVASCRIPT.INFO 이벤트 위임

 

 

댓글