자바스크립트 딥다이브 - 이벤트

ChoiYongHyeun·2023년 12월 27일
0
post-thumbnail

이벤트

브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트를 발생시킨다.

예를 들어 클릭, 키보드 입력, 마우스 이동 등이 일어나면 브라우저는 이를 감지하여 특정 타입의 이벤트를 발생 시킨다.

이 때 일어나는 이벤트에 반응하여 어떤 특정한 일을 하고 싶다면

해당하는 타입의 이벤트가 발생 했을 때 호출될 함수를 브라우저에게 위임하여, 해당 함수를 이벤트 때 마다 호출 할 수 있다.

이 때 호출될 함수를 이벤트 핸들러 라고 하고, 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록 라고 한다.

정리

브라우저에서 이벤트가 일어났을 때 호출될 함수를 이벤트 핸들러 라고 하고, 브라우저에게 해당 함수를 호출 시키도록 위임하는 것을 이벤트 핸들러 위임 이라고 한다.

예시를 통해 살펴보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p>0</p>
    <button>Click me !</button>
    <script>
      // Node 설정
      const $p = document.querySelector('p');
      const $button = document.querySelector('button');
	  // 버튼이 눌릴 때 마다 p 태그의 text 가 변경되도록 설정 
      $button.onclick = () => {
        const current = parseInt($p.textContent);
        $p.textContent = current + 1;
      };
    </script>
  </body>
</html>

버튼 태그가 눌릴 때 마다 동적으로 p 태그의 textcontent 가 변경된다.

이처럼 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍 이라고 한다.

댕재밌다. 이벤트 들어가니까

이벤트 타입

그럼 발생 할 수 있는 이벤트 타입들은 뭐가 있을까 ?

이벤트 타입은 약 200여가지로 매우 많지만 사용빈도가 높은 이벤트들을 테이블 형태로 정리해보았다.

마우스 이벤트

이벤트 명 설명
click 마우스 버튼을 클릭할 때 발생
contextmenu 마우스 오른쪽 버튼을 클릭할 때 발생
dblclick 마우스 버튼을 빠르게 두 번 클릭할 때 발생
mousedown 마우스 버튼을 누르는 순간 발생
mouseup 마우스 버튼을 눌렀다 뗄 때 발생
mousemove 마우스를 움직일 때 발생
mouseenter 마우스가 요소로 들어갈 때 발생 (하위 요소로 이동 시는 발생하지 않음)
mouseleave 마우스가 요소를 떠날 때 발생 (하위 요소로 이동 시는 발생하지 않음)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Follow Cursor</title>
    <style>
      #cursor {
        width: 20px;
        height: 20px;
        background-color: red;
        border-radius: 50%;
        position: absolute;
      }
    </style>
  </head>
  <body>
    <div id="cursor"></div>
    <script>
      const cursor = document.getElementById('cursor');

      document.addEventListener('mousemove', (event) => {
        const x = event.clientX;
        const y = event.clientY;

        cursor.style.left = x + 'px';
        cursor.style.top = y + 'px';
      });
    </script>
  </body>
</html>

응용을 통해 다음과 같은 것을 만들 수 있다.

대박 신기하다. 킹피티 ..
div 태그를 생성한 후 마우스의 움직임에 따라 해당 태그인 div 의 위치를 동적으로 변경하는구나
addEventListner 에 대해서도 더 공부해야겠고, 이벤트 핸들러 별로 프로퍼티가 존재하는구만
이벤트 핸들러 별 프로퍼티들도 사용 할 때 마다 공부해봐야겠다.

키보드 이벤트

이벤트 명 설명
keydown 키보드 키를 눌렀을 때 발생
keyup 키보드 키를 뗐을 때 발생
keypress 키보드 키를 누르는 동안 계속 발생 (특정 키에 대해서는 작동하지 않을 수 있음)

키보드 키가 눌리고 떼는 것이 입력과 뭐가 다른가 생각했는데

입력 없이도 눌린 키보드 만을 이야기 하는것이다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Keyboard Events Example</title>
  </head>
  <body>
    <p>
      키보드 이벤트를 테스트해보세요. 현재 눌린 키는:
      <span id="currentKey">없음</span>
    </p>
    <script>
      const currentKeyElement = document.getElementById('currentKey');

      document.addEventListener('keydown', (event) => {
        // 'keydown' 이벤트가 발생하면 실행되는 코드
        currentKeyElement.textContent = event.key;
      });

      document.addEventListener('keyup', () => {
        // 'keyup' 이벤트가 발생하면 실행되는 코드
        currentKeyElement.textContent = '없음';
      });
    </script>
  </body>
</html>

포커스 이벤트

포커스란 웹 페이지에서 어떤 요소가 현재 활성화 되어 사용자의 입력을 받을 수 있는 상태를 의미한다.

포커스는 사용자의 키보드나 마우스 등으로 입력하거나 상호작용 할 때 해당 입력을 받을 수 있다.

이벤트 명 설명
focus 요소에 포커스가 들어왔을 때 발생
blur 요소에서 포커스가 빠져나갔을 때 발생
focusin 요소에 포커스가 들어왔을 때 발생 (버블링이 발생하지 않음)
focusout 요소에서 포커스가 빠져나갔을 때 발생 (버블링이 발생하지 않음)

폼 이벤트

이벤트 명 설명
submit 폼을 제출할 때 발생
reset 폼을 초기화할 때 발생
change 입력 값이 변경될 때 발생 (input, select, textarea 등에서 발생)
input 입력 요소의 값이 변경될 때 발생 (input, textarea 등에서 발생)
focus 입력 요소에 포커스가 들어왔을 때 발생
blur 입력 요소에서 포커스가 빠져나갔을 때 발생

값 변경 이벤트

이벤트 명 설명 발생 시점
input 입력 필드의 값이 변경될 때마다 발생 값이 입력될 때마다
change 입력 필드의 값이 변경되고 포커스가 빠져나갈 때 발생 값이 변경되고 해당 입력 필드에서 포커스가 빠져나갈 때

뷰 이벤트

이벤트 명 설명
resize 브라우저 창의 크기가 변경될 때 발생
scroll 스크롤이 발생할 때 발생
fullscreenchange 전체 화면 모드 변경 시 발생
fullscreenerror 전체 화면 모드로 진입할 때 오류가 발생하면 발생

이벤트 핸들러 등록

이벤트 핸들러를 등록하는 방법은 3가지이다.

이벤트 핸들러 어트리뷰트 방식

HTML 요소 의 어트리뷰트 중에는 이벤트에 대응하는 이벤트 핸들러 어트리뷰트가 있다.

이벤트 핸들러 어트리뷰트의 이름은 onclick 과 같이 on 접두사와 이벤트의 종류를 나타내는 이벤트 타입이 있다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      button {
        width: 200px;
        height: 100px;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <button onclick="sayHi('김동동')">인사하겠씁니다</button>
    <script>
      function sayHi(name) {
        window.alert(`안녕하세요 ${name}`);
      }
    </script>
  </body>
</html>

이벤트 핸들러 어트리뷰트 방식에서는 함수 선언문 자체를 할당한다는 포인트가 존재한다.

함수 선언문을 할당하는 이유는 함수 호출문을 할당하게 된다면

함수 호출문의 결과 값이 할당되기 때문이다.

이벤트 핸들러 어트리뷰트 값으로 할당한 문자열은 암묵적으로 생성되는 이벤트 핸들러의 함수 몸체이기에 여러 가지 이벤트 핸들러 어트리뷰트 값을 설정하는 것이 가능하다.

오래된 코드들에서 사용하기 때문에 현대에 와서 더는 사용하지 않는 것이 좋다고 하였지만

요즘 CBD(Component Based Development) 방식의 React/Svelte/Vue.js 같은 프레임워크/레이아웃 에서는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다.

와우

이벤트 핸들러 프로퍼티 방식

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      button {
        width: 200px;
        height: 100px;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <button>인사하겠씁니다</button>
    <script>
      const $button = document.querySelector('button');

      $button.onclick = () => {
        alert(`안녕하세요 김동동`);
      };
    </script>
  </body>
</html>

이벤트 핸들러 프로퍼티도 위 이벤트 핸들러 어트리뷰트 방식과 같이 on 으로 시작하는 핸들러 프로퍼티들을 찾아 메소드를 바인딩 하는 방식이다.

다음과 같이 나눠 살펴볼 수 있다.

결국 이벤트 핸들러 어트리뷰트 방식과 이벤트 핸들러 프로퍼티 방식 모두 동일하게 노드의 프로퍼티로 추가하여 이벤트 핸들러를 위임하는 방식이란 것은 똑같지만

이벤트 핸들러 프로퍼티 방식은 단 하나의 이벤트 핸들러만 바인딩 할 수 있다는 단점이 있다.

addEventListener 메소드 방식

addEventListener 방식은 위에서 설명한 두 가지 방식 (이벤트 어트리뷰트, 이벤트 프로퍼티)에 비해 최근에 나온 방식이면서 단점들을 해결한 메소드 방식이다.

첫 번째 매개 변수에는 이벤트 타입을 전달하는데 이 때는 on 이라는 접두사를 붙이지 않고 사용한다.

두 번째 매개 변수에는 이벤트 핸들러를 전달하고

세 번째 매개변수에는 버블링 단계의 이벤트를 캐치할지 말지에 대한 내용을 담는데 이는 추후에 설명하도록 하자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      button {
        width: 200px;
        height: 100px;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <button>인사하겠씁니다</button>
    <script>
      const $button = document.querySelector('button');

      $button.addEventListener('click', () => alert(`안녕하세요 김동동님`));
    </script>
  </body>
</html>

addEventListener 의 경우 이벤트 프로퍼티 방식과 다르게 여러가지 이벤트 핸들러를 할당 할 수 있다.

...
    <script>
      const $button = document.querySelector('button');

      $button.addEventListener('click', () =>
        console.log('1번째 이벤트 핸들러'),
      );
      $button.addEventListener('click', () =>
        console.log('2번째 이벤트 핸들러'),
      );
    </script>
...

또한 이벤트 어트리뷰트 방식이나 이벤트 프로퍼티 방식으로 추가된 핸들러와도 충돌하지 않는다.

    <button onclick="console.log('0번째 이벤트 핸들러')">인사하겠씁니다</button>
    <script>
      const $button = document.querySelector('button');

      $button.addEventListener('click', () =>
        console.log('1번째 이벤트 핸들러'),
      );
      $button.addEventListener('click', () =>
        console.log('2번째 이벤트 핸들러'),
      );
    </script>

addEventListner 는 등록된 순서로 이벤트 핸들러가 작동하며, 중복된 핸들러가 등록될 경우 하나의 핸들러만 등록된다.

이벤트 핸들러 확인

자바스크립트 측에서는 확인 할 수 없으나 개발자 도구에서 확인 가능하다.

이벤트 핸들러 제거

addEventListener 메소드로 등록된 이벤트 핸들러를 제거하기 위해서는

EventTarget.prototype.removeEventListener 를 사용하면 된다.

전달 하는 인수는 addEventListener 와 동일하나 addEventListener 를 사용 할 때 담은 인수들과 정확히 맞지 않는 경우에는 삭제에 실패한다.

따라서 무명 함수를 이벤트 핸들러로 등록하였을 때에는 삭제에 실패한다.

    <script>
      const $button = document.querySelector('button');

      $button.addEventListener('click', function foo() {});
      $button.addEventListener('click', function bar() {});

        $button.removeEventListener('click', foo, true); // 삭제는 과연 ~??
    </script>

이렇게 하면 과연 삭제에 성공했을까 ?

안됩니다.

addEventListener 형태와 동일해야 한다고 했다.

그러면 removeEventListener 에 핸들러를 function foo(){} 이렇게 똑같이 넣으면 될까 ?

안됩니다.

이벤트 핸들러를 참조하고 있는 객체를 넣어주면 된다.

그러니 이벤트 핸들러에 함수를 넣을 때는 함수를 할당한 객체를 넣어주자

    <script>
      const $button = document.querySelector('button');

      const foo = function () {};
      const bar = function () {};
      $button.addEventListener('click', foo);
      $button.addEventListener('click', bar);

      $button.removeEventListener('click', foo); // 정상적으로 삭제 됨
    </script>

혹은 기명 함수 자체를 addEventListener 에 넣어줘도 된다.

    <script>
      const $button = document.querySelector('button');

      function foo() {}
      function bar() {}
      $button.addEventListener('click', foo);
      $button.addEventListener('click', bar);

      $button.removeEventListener('click', foo);
    </script>

키킥 그러나 생성자 함수가 아닌 함수를 사용 할 때에는 화살표 함수를 사용하라 했으니 객체를 넣는 것이 좀 더 명확하다.

그러나 만약 나는 죽어도 함수 선언 하고 싶지 않다면 함수 선언문 자체에서 removeEventListener 를 사용 할 수도 있다.

그러나 argument.calle 는 코드 최적화를 방지하기 때문에 가급적 이벤트 핸들러의 참조나 변수는 자료구조에 저장하여 사용하자

addEventListener 로 추가하지 않은 이벤트 핸들러는 어떻게 제거하나요 ?

직접 노드명.onclick = null 로 설정하셈요

이벤트 객체

이벤트가 발생하면 이벤트에 관련된 다양한 정보를 담고 잇는 이벤트 객체가 동적으로 생성된다.

생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p>클릭해보셈 좌표 알아맞춰 볼게요</p>
    <em class="message"></em>
    <script>
      const $em = document.querySelector('.message');

      document.onclick = (e) => {
        $em.textContent = `X : ${e.clientX} , Y : ${e.clientY}`;
        console.log(e);
      };
    </script>
  </body>
</html>

무수히 많은 프로퍼티들 ..

매개변수 명은 어떤 것을 써도 상관 없지만 만약 이벤트 핸들러 어트리뷰트 형식을 사용하게 된다면 매개변수 명은 무조건 event 로 해야 한다.

이벤트 객체의 상속 구조

이벤트 핸들러가 호출되면 이벤트 객체가 상속된다고 하였다.

생성되는 이벤트 객체들 또한 프로토타입을 갖는다.

다양한 이벤트 타입들과 프로토타입 들이 존재하지만

이번 강의에서 배운 이벤트 객체의 프로토타입은

Object => Event => UIEvent 에서부터 분기되는 이벤트 들이다.

UIEvent 에서 분기되는 프로토타입들은 생성자 함수를 호출하여 이벤트 객체를 생성 할 수 있다.

let e = new Event('foo') // 가능 
let v = new FocusEvent('bar') // 이런 식

이벤트 객체를 이벤트 생성 함수로 사용 할 일은 적을 것 같으니 프로토타입을 가진다는 점만 캐치하고

공통적으로 나눠 갖는 프로토타입과 이벤트 타입에 따른 이벤트 객체 별 프로토타입을 살펴보자

이벤트 객체의 공통 프로퍼티

Event.prototype 에 정의되어 있는 이벤트 관련 프로퍼티는 모든 파생 이벤트 객체에 상속된다.

즉 마우스 , 키보드, 클릭, 입력 .. 등등 할 거 없이 모두가 공통적으로 갖는 공통 프로퍼티이다.

프로퍼티 설명 반환 값 타입
type 이벤트의 종류를 나타내는 문자열 string
target 이벤트의 실제 대상 요소를 가리킴 EventTarget
currentTarget 현재 이벤트가 발생한 요소를 가리킴 EventTarget
bubbles 이벤트가 버블링 단계를 통해 전파되면 true, 그 외에는 false boolean
cancelable 이벤트가 취소 가능하면 true, 그 외에는 false boolean
eventPhase 현재 이벤트가 발생한 단계 (캡처링, 타깃, 버블링)를 나타내는 숫자 (1, 2, 3 중 하나) number
defaultPrevented preventDefault()가 호출되었는지 여부 (true 또는 false) boolean
isTrusted 브라우저에서 생성한 이벤트인 경우 true, 스크립트로 생성된 경우 false boolean
composed 이벤트가 그림자 DOM 경로를 통해 외부로 나갈 수 있는 경우 true, 그 외에는 false boolean
timeStamp 이벤트가 발생한 시간을 나타내는 타임스탬프 DOMHighResTimeStamp

객체 공통 프로퍼티 중 target 에 대해서 알아보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="checkbox" />
    <em class="message"> NO </em>
    <script>
      const $input = document.querySelector('input[type = checkbox]');
      const $em = document.querySelector('em');
      const changeChecker = (e) => {
        $em.textContent = e.target.checked ? 'YES' : 'NO';
      };
      $input.addEventListener('change', changeChecker);
    </script>
  </body>
</html>

에서 changeChecker 함수의 경우 이벤트객체를 받아 이벤트 객체의 프로퍼티인 target 에 접근했다.

이벤트 객체의 target 은 이벤트의 실제 대상 요소이며 $input 과 동일하다.

마우스 정보 취득

프로퍼티 설명 반환 값 타입
clientX 브라우저 창을 기준으로 마우스의 X 좌표 number
clientY 브라우저 창을 기준으로 마우스의 Y 좌표 number
screenX 모니터 화면을 기준으로 마우스의 X 좌표 number
screenY 모니터 화면을 기준으로 마우스의 Y 좌표 number
pageX 문서 전체를 기준으로 마우스의 X 좌표 number
pageY 문서 전체를 기준으로 마우스의 Y 좌표 number
movementX 마우스의 X 좌표의 상대적인 변화량 number
movementY 마우스의 Y 좌표의 상대적인 변화량 number
buttons 눌려진 마우스 버튼에 대한 정보를 가진 정수 (비트 플래그) number
button 클릭된 마우스 버튼을 나타내는 코드 (0: 왼쪽, 1: 가운데, 2: 오른쪽) number

다음 고유의 프로퍼티들을 기준으로 마우스 드래그 한 영역에 따라 드래그 한 영역을 그리고, 영역의 넓이를 만드는 코드를 생성해보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0px;
      }
      .dragBox {
        background-color: rgba(255, 105, 180, 0.5);
        display: flex;
        position: absolute;
        justify-content: center;
        align-items: center;
        color: white;
      }
    </style>
  </head>
  <body>
    <script>
      let isDragging = true;
      let startX, startY, endX, endY;

      // 커서 이동에 따라 생성된 box 태그의 너비, 높이, 시작점을 변경하는 함수 생성

      const boxHandler = (event) => {
        const boxes = document.querySelectorAll('.dragBox');
        const $box = boxes[boxes.length - 1];
        if (isDragging) {
          const endX = event.clientX;
          const endY = event.clientY;
          const width = Math.abs(endX - startX);
          const height = Math.abs(endY - startY);

          $box.style.width = `${width}px`;
          $box.style.height = `${height}px`;
          $box.style.left = `${Math.min(startX, endX)}px`;
          $box.style.top = `${Math.min(startY, endY)}px`;

          const area = Math.round(width * height);

          // 박스의 면적 표기하기
          $box.textContent = `${area}px`;
          // 면적 글자 크기 변경하기
          $box.style.fontSize = `${Math.min(width, height) * 0.15}px`;
        }
      };

      // 마우스가 눌렸을 때 box 태그를 생성하는 이벤트 핸들러

      document.addEventListener('mousedown', (event) => {
        isDragging = true;
        // 마우스가 눌렸을 때 시작 위치를 지정해주기
        startX = event.clientX;
        startY = event.clientY;

        // 마우스가 눌렸을 때 box 태그를 body에 추가
        const $box = document.createElement('p');
        $box.classList.add('dragBox');
        document.body.appendChild($box);

        document.addEventListener('mousemove', boxHandler);
      });

      // 마우스를 놓았을 때 box 태그의 변화를 그만 만드는 이벤트 핸들러
      // 새로운 box 를 만들기 위함
      document.addEventListener('mouseup', () => {
        isDragging = false;
      });
    </script>
  </body>
</html>

키보드 정보 취득

프로퍼티 설명 반환 값 타입
key 눌린 키의 식별자 string
code 눌린 키의 물리적인 위치를 기반으로 하는 식별자 string
location 키의 물리적인 위치 (0: 일반, 1: 왼쪽, 2: 오른쪽, 3: 넘패드 등) number
ctrlKey Ctrl 키가 눌렸는지 여부 boolean
shiftKey Shift 키가 눌렸는지 여부 boolean
altKey Alt 키가 눌렸는지 여부 boolean
metaKey 메타 키 (예: Windows 키 또는 Command 키)가 눌렸는지 여부 boolean
repeat 키가 반복된 이벤트인지 여부 boolean

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Real-time Input</title>
    <style>
      body {
        display: flex;
      }
      textarea {
        flex: 1;
        width: 50vh;
      }
      div {
        flex: 1;
        color: white;
        background-color: black;
        width: 50vh;
        height: 100vh;
        white-space: pre-line; /* 줄바꿈 허용 설정 */
      }
    </style>
  </head>
  <body>
    <textarea id="input" cols="30" rows="10"></textarea>
    <div id="output"></div>

    <script>
      const $input = document.getElementById('input');
      const $output = document.getElementById('output');

      $input.addEventListener('input', (event) => {
        $output.textContent = event.target.value;
      });
    </script>
  </body>
</html>

간단하게 textarea 내부에 존재하는 글을 실시간으로 반영해주는 태그를 작성해봤다.

벨로그나 노션처럼 자동으로 마크업 언어로 변경해주는걸 하려고 했는데
엔터키에서 난관을 겪었다.. `

이벤트 전파

  <script>
    const $body = document.querySelector('body');
    const $main = document.querySelector('main');
    const $div = document.querySelector('div');
    const $span = document.querySelector('span');
    const $p = document.querySelector('p');

    const tagArr = [$body, $main, $div, $span, $p];

    tagArr.forEach((tag) => {
      tag.addEventListener(
        'click',
        (event) => {
          console.log(
            `${tag.tagName} click !` + `eventPahse : ${event.eventPhase}`,
          );
        },
        true,
      );

      tag.addEventListener(
        'click',
        () => {
          console.log(
            `${tag.tagName} click !` + `eventPahse : ${event.eventPhase}`,
          );
        },
        false,
      );
    });
  </script>

다음처럼 태그가 클릭 되면 본인의 태그명eventPhase 를 로그하는 이벤트 핸들러를 위임했다.

이벤트는 최상단 태그부터 이벤트가 일어난 태그까지 하위 태그들을 지나며 전달 되고, 전달 된 이후에는 최상단 태그까지 다시 전달된다.

이처럼 최상단에서 이벤트가 일어난 태그 전까지 내려가는 단계를 이벤트 캡쳐링 페이즈 라고 하고

이벤트가 일어난 태그에서의 단계를 타겟 페이즈

이벤트가 일어난 태그 이후부터 최상위 단게까지를 이벤트 버블링 페이즈 라고 한다.

그러니 어떤 특정 태그에서 이벤트가 발생하면, 해당 태그에서만 이벤트가 발생하는게 아니라 최상단부터 해당 이벤트를 통해 전달 된다.

addEventListener 에서 3번째 인수인 boolean 값은 이벤트 캡쳐링 페이즈에서 이벤트를 전파 할 것인가, 아닌가를 의미한다.

만약 3번째 인수를 true 로 설정하면, 이벤트 캡쳐링 페이즈 에서의 이벤트를 전파하고, 이벤트 버블링 페이즈에서는 전파하지 않는다.

false 로 할 경우 반대이다.

나는 모든 단계를 알기 위해 true , false 모두 이벤트 핸들러에 추가해주었다.

이 때 p태그를 클릭해보자

이벤트 페이즈들은 다음과 같다 .

  • 1 : 이벤트 캡쳐링 페이즈
  • 2 : 타겟 페이즈
  • 3 : 이벤트 버블링 페이즈

그러니 코드를 보면 최상단에서부터 p 태그까지 이벤트가 캡쳐링되고 p 태그에서 타겟 페이즈가 일어나고

타겟 페이즈가 두 번 일어난건 리스너가 두개라 그렇다. 큰 의미는 없다.

이후에는 최상단까지 이벤트 버블링이 일어났다.

아니 그러면 인수를 뭐로 설정하든 버블링이나 캡쳐링 둘 중 전파가 일어나는거네 ?

맞습니당

하지만 방법을 통해 전파를 막을 수 있어요

이벤트 핸들러가 호출될 때 이벤트 객체가 생성된다고 했었다.

생성되는 이벤트 객체의 메소드 중 .stopPropagation 을 사용하면 이벤트 버블링 페이즈 자체를 중단 할 수 있다.

그럼 addEventListener 할 때 세 번쨰 인수를 false 로 해줘 이벤트 버블링 페이즈 에서만 전파가 일어나게 설정하고, .stopPropagation 을 실행시켜주면 타겟 페이즈 일 때만 이벤트가 전파된다.

    tagArr.forEach((tag) => {
      tag.addEventListener(
        'click',
        (event) => {
          event.stopPropagation(); // 버블링 페이즈 자체를 차단
          console.log(
            `${tag.tagName} click !` + `eventPahse : ${event.eventPhase}`,
          );
        },
        false, // 버블링 페이즈에서만 이벤트가 전파되게 설정
      );
    });

굿 ~

이벤트 위임

책에서는 좀 더 어려운 예제를 들었지만 간단한 예제를 통해 위임에 대해 이해해보자

만약 ul 태그 안에 존재하는 li 태그들이 존재하고

이 때 li 태그를 클릭하면 클릭 된 텍스트 노드의 값을 로그하도록 해보자

ㅋㅋ 아 ~ 반복문으로 ul 태그의 자식 노드들에게 모두 이벤트 핸들러 할당하면 되는거 아니냐고

    <script>
      const $ul = document.querySelector('#fruits');
      [...$ul.children].forEach((tag) => {
        tag.addEventListener('click', (event) => {
          console.log(event.target.textContent);
        });
      });
    </script>

이렇게 할 수도 있다.

그런데 아주 극단적으로 자식 태그가 999999999999개 있으면 9999999999개의 자식 노드들에게 모두 이벤트 핸들러를 위임하는 방법보다 효율적인 방법들이 존재한다.

그건 바로 ..

    <script>
      const $ul = document.querySelector('#fruits');
      $ul.addEventListener('click', (event) => {
        console.log(event.target.textContent);
      });
    </script>

부모노드에게 이벤트 핸들러를 할당 시키는 것이다.

왜 ?!?!!??!!??!

나도 처음에 보고 도저히 왜 부모 노드에 할당 시키는 것이 하위 노드의 이벤트까지 캡쳐 할 수 있는 것인지 당최 이해가 안갔다.

분명 위에서는 타겟 페이지까지만 이벤트 캡쳐 가 일어나고 타겟 페이지에서부터는 다시 상위 노드를 향해 이벤트 버블링 이 일어나는거 아닌가 ? 싶었다.

그런데 생각해보자

위에서 이 사진을 생각해보자

여기서 p 태그를 눌러도 버블링 단계에서 이벤트 전파가 일어나서 상위 노드인 span , div , main , body 태그에서도 이벤트가 일어났던 것을 기억 할 수 있다.

해당 태그에서도 그렇다.

DOM 에서 ul 태그에만 할당 되어 있는 이벤트 핸들러는 하위 태그에서 올라오는 이벤트를 전파 받아 이벤트 핸들러가 호출되고 이벤트 객체가 생성된다.

이벤트 객체에는 여러 프로퍼티가 있는데 event.target 프로퍼티는 이벤트가 일어난 노드를 가리킨다고 했다.

event.curentTarget 은 이벤트 핸들러가 할당되어 있는 노드를 가리킨다.

    <script>
      const $ul = document.querySelector('#fruits');
      $ul.addEventListener('click', (event) => {
        event.stopPropagation();
        console.log(event.target.textContent);
        console.log(`event.target : ${event.target.tagName}`);
        console.log(`event.curentTarget : ${event.currentTarget.tagName}`);
      });
    </script>

정리

이벤트 위임 은 부모 노드에 존재하는 이벤트 핸들러가 하위 노드의 이벤트를 이벤트 버블링 단계 혹은 이벤트 캡쳐링 단계에서 이벤트를 확인하고, 생성된 이벤트 객체의 프로퍼티인 target 에서 이벤트가 일어난 하위노드를 바인딩 하는 것을 의미한다.

DOM 요소의 기본 동작 조작

DOM 요소의 기본 동작 중단

a 태그나 checkbox 또는 radio 요소를 클릭하면 체크 또는 해제 된다.

이벤트 객체의 preventDefault 메소드는 이런 DOM 요소의 기본 동작을 중단시킨다.

MDN 에서의 preventDefault 설명

이벤트는 전파 되지만 이벤트의 기본 동작 자체를 못하게 한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="checkbox" />
    <div></div>
    <script>
      const $checkbox = document.querySelector('input[type = checkbox]');
      const $div = document.querySelector('div');
      console.log($div);

      $checkbox.addEventListener('click', (event) => {
        event.preventDefault();
        $div.innerHTML += 'preventDefault() 때문에 체크가 불가능해요<br>';
      });
    </script>
  </body>
</html>

이런 기능이 왜 필요한가 싶기도 했지만

조건에 따라 기능을 막을 때 유용하다.

예를 들어 회원 가입 할 때 영어 소문자만 사용 할 수 있다고 해보자

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      input {
        width: 200px;
      }
    </style>
  </head>
  <body>
    <form action="">
      <input type="text" placeholder="영어 소문자만 입력해주세요" />
    </form>
    <p></p>
    <script>
      const $input = document.querySelector('input[type = "text"]');
      const $p = document.querySelector('p');

      const displayWarning = (charcode) => {
        $p.textContent = `영어 소문자만 입력해주세요 현재 charCode : ${charcode}`;
      };

      $input.addEventListener('keypress', (event) => {
        const charcode = event.charCode;
        if (charcode === 0) return;
        if (charcode < 97 || charcode > 127) {
          event.preventDefault();
          displayWarning(charcode);
        }
      });
    </script>
  </body>
</html>

stopPropagation

이 내용은 위에서 했으니 패스 ~!

이벤트 핸들러 내부의 this

this .. 왜이렇게 날 괴롭히는거야

이벤트 핸들러 어트리뷰트 방식에서의 this

이벤트 핸들러 내부의 this 도 일반 함수, 메소드, 화살표 함수의 this 바인딩 방식과 동일한 로직을 갖는다.

몇 가지를 제외하고 ..

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      button {
        width: 100px;
        height: 100px;
      }
    </style>
  </head>
  <body>
    <button onclick="handleClick()"></button>
    <script>
      function handleClick() {
        console.log(this);
      }
    </script>
  </body>
</html>

이벤트 핸들러 어트리뷰트 방식으로 버튼을 클릭 할 때 this 를 로그하게 만들었다.

일반 함수 환경에서 선언된 this 는 전역 객체를 가리킨다고 했었기 때문에 전역 객체인 window 를 가리킨다.

다만 이벤트 핸들러 어트리뷰트 방식에서 인수로 전달하게 되면 바인딩한 DOM 요소를 가리킨다.

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Document</title>
   <style>
     button {
       width: 100px;
       height: 100px;
     }
   </style>
 </head>
 <body>
   <button onclick="handleClick(this)"></button>
   <script>
     function handleClick(button) {
       console.log(button);
     }
   </script>
 </body>
</html>

이벤트 핸들러 프로퍼티 방식과 addEventListener 방식에서의 this

중요한 개념을 먼저 다시 생각하고 가자

화살표 함수가 아닌 일반 함수에서의 this 는 호출되는 당시에 결정된다고 했다.

함수가 호출 될 때 함수의 this 는 함수가 바인딩 된 객체를 가리키게 된다고 했다.

이거 하나 잘 기억하고 가자

이번에는 예시를 조금 꼬아서 버튼이 눌리면 버튼에 눌린 횟수가 작성되게 해보자

<!DOCTYPE html>
...
  <body>
    <button id="button1">0</button>
    <button id="button2">0</button>
    <script>
      const $button1 = document.querySelector('#button1');
      const $button2 = document.querySelector('#button2');

      // 이벤트 핸들러 프로퍼티 방식
      $button1.onclick = function () {
        const num = parseInt(this.textContent);
        this.textContent = num + 1;
      };
      // addEventListener 방식
      $button2.addEventListener('click', function () {
        const num = parseInt(this.textContent);
        this.textContent = num + 1;
      });
    </script>
  </body>
</html>

이번에는 잘 작동한다 . 왤까 ?

이벤트 핸들러 프로퍼티 방식, addEventListener 방식 모두 일반 함수 형태로 선언해주었는데

이는 결국 DOM 에 있는 객체인 $button1 , $button2의 메소드로 바인딩 시킨 것이기 때문에 this 는 바인딩 된 객체인 $button1 , $button2 를 가리킨다.

화살표 함수로 선언하면 어떻게 될까 ?

      $button1.onclick = () => {
        const num = parseInt(this.textContent);
        this.textContent = num + 1;
      };
      // addEventListenr 방식
      $button2.addEventListener('click', () => {
        const num = parseInt(this.textContent);
        this.textContent = num + 1;
      });

안된다. 화살표 함수는 호출 될 때가 아니라 선언될 때의 렉시컬 환경에 의해 바인딩 되기 때문에

현재 전역 환경에서 선언 되어 thiswindow 를 가리킨다 .

정리

일반 함수 형태로 DOM 노드에 이벤트 핸들러를 위임 할 때에는 DOM 노드 객체의 메소드로 등록되어 this 는 해당 객체인 DOM 노드 를 가리킨다.

화살표 함수의 this 는 선언 될 때의 렉시컬 환경을 고려하기 때문에 선언된 환경의 상위 객체를 가리킨다.

그럼 렉시컬 환경을 좁혀서 Class 내에서 바인딩 시켜보자

  <body>
    <button id="button">0</button>
    <script>
      const $button = document.querySelector('button');

      class Button {
        constructor() {
          this.cnt = 0;
          this.$button = $button;
          this.$button.onclick = this.increase;
        }

        increase() {
          console.log(this);
          this.cnt += 1;
          this.$button.textContent = this.cnt;
        }
      }

      new Button();
    </script>
    <script></script>
  </body>

클래스를 이용해서 $button 의 이벤트 핸들러를 지정해주었다.

마치 코드만 보면

음 ~ class 내부에서 메소드로 정의된 increase() 에서의 thisconstructor() 로 생성되는 인스턴스를 가리키니 increase() 에서의 this{this.cnt = 0 , this$button = $button ...} 이겠구나 ~

그러면 this.cnt 를 1 늘린 후에 this.$button.textContent = this.cnt 로 하는건 문제가 없겠다 싶다.

그런데 오류가 난다. 로그된 것을 보면 increasethis{} 인스턴스가 아니라 $button 을 가리키고 있다.

그 이유는 constructor 내부에서 this.$button.onclick = this.increase; 이 부분에 의해 발생하는데

메소드인 increase 는 결국 $button의 이벤트 핸들러로 바인딩 되었다.

그럼 $button 이 눌릴 때 실행되는 increasethis{} 가 아닌 $button 이 되고

this.cnt += 1undefined + 1 이니 NaN
this.$button.textContent = this.cnt$button.$button.textContent 가 되어 오류가 발생한다.

정리

내용이 조금 복잡하다. 정리해보자
클래스 내부에서 메소드로 선언된 increasethis 는 호출 될 때 결정된다.
$button 의 이벤트 핸들러로 등록된 increase 는 호출 될 때 this$button 과 바인딩 시킨다.

그럼 어떻게 해결할까 ?

1. this 바인딩 하기 (bind())

    <script>
      const $button = document.querySelector('button');

      class Button {
        constructor() {
          this.cnt = 0;
          this.$button = $button;
          this.$button.onclick = this.increase.bind(this);
        }

        increase() {
          console.log(this);
          this.cnt += 1;

          this.$button.textContent = this.cnt;
        }
      }

      new Button();
    </script>

하나는 바로 이벤트 핸들러를 등록 할 때 increase 가 가리키는 thisconstructor 내부의 this 로 바인딩 시켜버리는 것이다.

그러면 호출되는 increasethisButton Object 를 가리켜 잘 작동한다.

그럼 이렇게 하면 될까 ?

화살표 함수 쓰기

bind 도 결국엔 this 를 바인딩 하기 위해 사용한거다.

바인딩은 왜했는데 ? Button Object ({this.cnt , ... }) 를 가리키기를 원해서!!

그럼 그냥 화살표 함수를 쓰자

    <script>
      const $button = document.querySelector('button');

      class Button {
        constructor() {
          this.cnt = 0;
          this.$button = $button;
          this.$button.onclick = this.increase;
        }

        increase = () => {
          console.log(this);
          this.cnt += 1;
          this.$button.textContent = this.cnt;
        };
      }

      new Button();
    </script>

화살표 함수에서의 this 는 정적으로 상위 렉시컬 환경의 this 와 동일하게 바인딩 되기 때문에

어차피 생성될 인스턴스 (constructor(){}로 생성되는) 를 바인딩 한다.

아 굿 ~

다만 이 파트를 공부하면서 느낀 것은 이렇게 버튼의 이벤트 핸들러를 클래스를 이용해서 캡슐화 시키는 이유가 뭘까 ? 를 고민하게 됐다.
쫄래 쫄래 챗지피티한테 가서 물어봤는데

라고 한다.
생각보다 엄청나게 좋은 예시였다.

이벤트 핸들러 인수 전달

이벤트 핸들러를 어트리뷰트 방식으로 전달 할 때에는 함수 선언문을 할당 하기 때문에 인수를 전달 하는 것이 가능하지만

프로퍼티 방식이나 addEventListener 로 전달 할 때에는 인수 전달 하는 것이 불가능하다.

하지만 이는 콜백 함수 를 이용해서 전달 하는 것이 가능하다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <em id="msg"></em>

    <script>
      const $input = document.querySelector('input[type = "text"]');
      const $msg = document.querySelector('em');
      const MIN_LENGTH = 5;

      // 인수를 받는 콜백 함수 생성
      const checkMinLength = (min) => {
        const textLength = $input.value.length;
        console.log(textLength);

        $msg.textContent =
          textLength >= min ? '' : `최소 ${min}자 이상 작성해주세요`;
      };

      // 이벤트 핸들러에서 인수를 받는 콜백 함수 실행
      $input.addEventListener('blur', () => {
        checkMinLength(MIN_LENGTH);
      });
    </script>
  </body>
</html>

구우웃 ~

커스텀 이벤트

커스텀 이벤트 부분은 해당 링크를 참고하자

나도 이 부분은 오래 공부해야 할 것 같다..

https://ko.javascript.info/dispatch-events

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글