Chapter 40

40.1 이벤트 드리븐 프로그래밍

  • 이벤트 핸들러 : 이벤트 발생시 호출될 함수

이벤트 발생시 이벤트 핸들러를 호출하도록 브라우저에게 위임할 수 있다. 이런 이벤트를 중심으로 프로그램의 흐름을 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라 한다.

40.2 이벤트 타입

이벤트 타입의 종류는 굉장히 많고, 모든 것을 다 공부하는것 보다 상황에 맞게 찾아서 공부하는 것이 더 좋다고 판단하여 생략한다.

40.3 이벤트 핸들러 등록

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

<body>
  <button onclick="introduce('Jinwon')">btn</button>
  <script>
    function introduce(name){
    	console.log(`My name is ${name}`);
    }
  </script>
</body>

이벤트 핸들러 어트리뷰트 값은 암묵적으로 생성될 이벤트 핸들러의 함수 몸체를 의미한다. 이벤트 핸들러에 인수를 전당하기 위해 이렇게 동작한다.

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

이벤트 핸들러 프로퍼티에 함수를 하인딩하여 이벤트 핸들러를 등록한다.

<body>
  <button>btn</button>
  <script>
    const $btn = document.querySelector('button');
    
    $btn.onclick = function () {
    	console.log(`Click btn`);
    }
  </script>
</body>

이벤트 핸들러 등록을 위해서는 이벤트 타깃, 이벤트 타입, 이벤트 핸들러를 지정해야 한다. 위 예시에서 $btn은 이벤트 타깃, onclick은 이벤트 타입, function은 이벤트 핸들러에 해당한다.

  • 장점 : HTML과 자바스크립트가 뒤섞이는 이벤트 핸들러 어트리뷰트 방식의 문제점을 해결할 수 있다
  • 단점 : 하나의 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만 바인딩할 수 있다.

40.3.3 addEventListener 메서드 방식

<body>
  <button>btn</button>
  <script>
    const $btn = document.querySelector('button');
    
    // 이벤트 핸들러 프로퍼티 방식
    $btn.onclick = function () {
    	console.log(`Click btn`);
    }
    
    // addEventListner 메서드 방식
    $btn.addEventListener('click', function(){
    	console.log(`Click btn`);
    });
  </script>
</body>

마지막 파라미터에 false를 지정하면 버블링 단계에서, true를 지정하면 캡처링 단계에서 이벤트를 캐치한다.

  • 장점 : 동일한 HTML 요소에서 발생한 동일한 이벤트에 대해 하나 이상의 이벤트 핸들러를 등록할 수 있다. 하지만, 동일한 이벤트 핸들러를 중복 등록하면 하나의 이벤트 핸들러만 등록된다.

40.4 이벤트 핸들러 제거

<body>
  <button>btn</button>
  <script>
    const $btn = document.querySelector('button');
    const handleClick = () => console.log(`Click btn`);
    
    $btn.addEventListener('click', handleClick);
    
    //addEventListener에 전달한 인수와 removeEventListener에 전달한 인수가 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.
    $btn.removeEventListener('click', handleClick, true); //X
    $btn.removeEventListener('click', handleClick); //O 
  </script>
</body>

addEventListener에 인수로 전달한 이벤트 핸들러와 removeEventListener에 인수로 전달한 이벤트 핸들러는 동일해야 한다. 따라서, 무명 함수를 이벤트 핸들러로 등록하면 제거할 수 없다. 다음과 같이 기명 이벤트 핸들러 내부에서 removeEventLisstener를 호출하여 이벤트 핸들러를 제거하는 것은 가능하다.

$btn.addEventListener('click', function handleClick(){
  console.log(`Click btn`);
  
  $btn.removeEventListener('click', handleClick);
});

기명 함수를 이벤트 핸들러로 등록할 수 없다면 argument.callee를 사용할 수도 있다.

$btn.addEventListener('click', function (){
  console.log(`Click btn`);
  
  $btn.removeEventListener('click', argument.callee);
});

이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는 removeEventListener로 제거할 수 없고, 이벤트 핸들러 프로퍼티에 null을 할당해야 한다.

<body>
  <button>btn</button>
  <script>
    const $btn = document.querySelector('button');
    const handleClick = () => console.log(`Click btn`);
    
    $btn.onclick = handleClick;
    
    $btn.onclick = null;
  </script>
</body>

40.5 이벤트 객체

이벤트 발생시 이벤트 객체가 동적으로 생성되고, 생성된 이벤트 객체는 이베트 핸들러의 첫 번째 인수로 전달된다.

<!-- 이벤트 핸들러 어트리뷰트 방식은 event가 아닌 다른 이름으로 이벤트 객체를 전달받지 못한다. -->
<body onclick="showPosition(event)">
  <p>클릭한 위치의 값이 출력된다.</p>
  <script>
    const $msg = document.querySelector('.button');
    
    function showPosition(e) {
    	$msg.textContent = `x: ${e.clientX}, y: ${e.clientY}`
    }
  </script>
</body>

40.5.1 이벤트 객체의 상속 구조


위 그림에서 타입들은 모두 생성자 함수다.
생성자 함수로 생성된 이벤트 객체들은 프로토타입 체인으로 연결된다.

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

  • type : 이벤트타입
  • target : 이벤트를 발생시킨 DOM 요소
  • currentTarget : 이벤트 핸들러가 바인딩된 DOM 요소
  • eventPhase : 이벤트 전파 단계
  • bubbles : 이벤트를 버블링으로 전파하는지 여부
  • cancelable : preventDefault 메서드를 호출하여 이벤트의 기본 동작을 취소할 수 있는지 여부
  • defaultPrevented : preventDefault 메서드를 호출하여 이벤트를 취소했는지 여부
  • isTrusted : 사용자의 행위에 의해 발생한 이벤트인지 여부
  • timeStamp : 이벤트가 발생한 시각

40.5.3 마우스 정보 취득

  • 마우스 포인터 좌표 정보를 나타내는 프로퍼티 : screenX/screenY, clientX/clientY, pageX/pageY, offsetX/offsetY
  • 버튼 정보를 나타내는 프로퍼티 : altKey, ctrlKey, shiftyKey, button

40.5.4 키보드 정보 취득

  • altKey, ctrlKey, shiftyKey, metaKey, key, keyCode

40.6 이벤트 전파

<body>
  <ul id="numbers">
    <li id="one">one</li>
    <li id="two">two</li>
    <li id="three">three</li>
  </ul>
</body>

li 태그 클릭시 클릭 이벤트 객체가 생성되고, 이벤트를 발생시킨 DOM 요소(이벤트 타겟)을 중심으로 DOM 트리를 통해 전파된다.

  • 캡처링 단계 : 이벤트가 상위 요소에서 하위 요소 방향으로 전파
  • 타겟 단계 : 이벤트가 타켓에 도달
  • 버블링 단계 : 이벤트가 하위 요소에서 상위 요소 방향으로 전파
<body>
  <ul id="numbers">
    <li id="one">one</li>
    <li id="two">two</li>
    <li id="three">three</li>
  </ul>
  <script>
    const $numbers = document.getElementById('numbers');
    
    // li를 클릭한 경우
    $numbers.addEventListener('click', e => {
    	console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
        console.log(`이벤트 타겟: ${e.target}`); //[object HTMLLIElement]
        console.log(`커렌트 타켓: ${e.currentTarget}`); /[object HTMLListElement]
    });
  </script>
</body>

이벤트 핸들러 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타겟 단계와 버블링 단계의 이벤트만 캐치할 수 있다. 하지만, addEventListener로 등록한 이벤트 핸들러는 캡처링 단계의 이벤트도 선별적으로 캐치할 수 있다.

<body>
  <ul id="numbers">
    <li id="one">one</li>
    <li id="two">two</li>
    <li id="three">three</li>
  </ul>
  <script>
    const $numbers = document.getElementById('numbers');
    
    // li를 클릭한 경우 캡처링 단계의 이벤트를 캐치한다.
    $numbers.addEventListener('click', e => {
    	console.log(`이벤트 단계: ${e.eventPhase}`); // 1: 캡처링 단계
        console.log(`이벤트 타겟: ${e.target}`); //[object HTMLLIElement]
        console.log(`커렌트 타켓: ${e.currentTarget}`); /[object HTMLListElement]
    },true);
  </script>
</body>

정리하자면, 이벤트는 이벤트를 발생시킨 타겟 뿐 만 아니라 상위 DOM 요소에서도 캐치할 수 있다.

대부분의 이벤트는 캡처링과 버블링을 통해 전파되지만, 예외적으로 몇 가지 이벤트는 버블링되지 않는다.

  • 포커스 이벤트 : focus/blur
  • 리소스 이벤트 : load/unload/abort/error
  • 마우스 이벤트 : mouseenter/mouseleave

40.7 이벤트 위임

이벤트 위임은 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다.

<body>
  <ul id="numbers">
    <li id="one" class="active">one</li>
    <li id="two">two</li>
    <li id="three">three</li>
  </ul>
  <div>선택된 아이템: <em class="msg">one</em></div>
  <script>
    // 사용자 클릭에 의해 선택된 내비게이션 아이템에 active 클래스를 추가하고 그 외의 아이템의 active 클래스 제거
    const $numbers = document.getElementById('numbers');
    const $msg = document.getElementById('.msg');
    
    function activate({target}){
    	// 이벤트를 발생시킨 요소가 자식이 아니라면 무시
    	if (!target.matches('#fruits > li')) return;
    
    	[...$numbers.children].forEach($number => {
    		$fruit.classList.toggle('active', $number === target);
    		$msg.textContent = target.id;
    	});
    }
    
    // 이벤트 위임
    $numbers.onclick = activate;
  </script>
</body>

40.8 DOM 요소의 기본 동작 조작

40.8.1 DOM 요소의 기본 동작 중단

preventDefault 메서드를 이용해 DOM 요소의 기본 동작(ex a, checkbox 요소의 기본 동작들)을 중단시킨다.

40.8.2 이벤트 전파 방지

stopPropagation 메서드를 이용해 이벤트 전파를 중지시킨다.

40.9 이벤트 핸들러 내부의 this

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

<body>
  <button onclick="handleClick(this)">click me</button>
  <script>
    function handleClick(button){
    	console.log(button); // 이벤트를 바인딩한 button요소
    	console.log(this); // window
    }
  </script>
</body>

함수 내부의 this는 전역 객체를, 인수로 전달한 this는 이벤트를 바인딩한 DOM요소를 가리킨다.

40.9.2 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식

<body>
  <button class="btn1">btn1</button>
  <button class="btn2">btn2</button>
  <script>
    const $btn1 = document.querySelector('.btn1');
    const $btn2 = document.querySelector('.btn2');
    
    // 이벤트 핸들러 프로퍼티 방식
    $btn1.onclick = function (e) {
    	// this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
    	console.log(this); // $btn1
    	console.log(e.currentTarget); // $btn1
    	console.log(this === e.currentTarget); // true
    }
    
    // addEventListener 방식
    $btn2.addEventListener('click', function (e) {
    	// this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
    	console.log(this); // $btn2
    	console.log(e.currentTarget); // $btn2
    	console.log(this === e.currentTarget); // true
    });
  </script>
</body>

두 방법 다 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 하지만, 화살표 함수로 정의한 이벤트 핸들러 내부의 this는 항상 상위 스코프의 this를 가리킨다.

클래스에서 이벤트를 바인딩하는 경우 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리키기 때문에 메서드 내부의 this가 클래스가 생성할 인스턴스를 가리키도록 하려면 다음과 같이 코드를 작성해야 한다.

<body>
  <button class="btn">0</button>
  <script>
  	class App {
    	constructor() {
    		this.$btn = document.qureySelector('.btn');
    		this.count = 0;
    		
    		// increase 메서드를 이벤트 핸들러로 등록
    		// 메서드 내부의 this가 인스턴스를 가리키도록 한다.
    		this.$btn.onclick = this.increase.bind(this);
    	}
    
    	increase() {
    		this.$btn.textContent = ++this.count;
    	}
    	
    	// 다음과 같이 화살표 함수를 사용해서 메서드 내부의 this가 인스턴스를 가리키도록 할 수도 있다.
    	// 즉, bind 할 필요 없이
    	// this.$btn.onclick = this.increase;
    	// 이렇게 사용할 수 있다.
    	// increase = () => this.$btn.textContent = ++this.count;
    }
    
    new App();
  </script>
</body>

40.10 이벤트 핸들러에 인수 전달

  1. 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
<body>
  <label>User name<input type="text"></label>
  <em class="message"></em>
  <script>
  	const MIN_NAME_LENGTH = 3;
    const $input = document.querySelector('input[type=text]');
    const $msg = document.querySelector('.message');
    
    const checkUserNameLength = min => {
    	$msg.textContent = $input.value.length < min ? `${min}이상 입력해주세요` : '';
    }
    // 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
    $input.onblur = () => {
    	checkUserNameLength(MIN_NAME_LENGTH);
    }
  </script>
</body>
  1. 이벤트 핸들러를 반환하는 함수를 호출하면서 인수를 전달한다.
<body>
  <label>User name<input type="text"></label>
  <em class="message"></em>
  <script>
  	const MIN_NAME_LENGTH = 3;
    const $input = document.querySelector('input[type=text]');
    const $msg = document.querySelector('.message');
    
    const checkUserNameLength = min => e => {
    	$msg.textContent = $input.value.length < min ? `${min}이상 입력해주세요` : '';
    }
    // 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
    $input.onblur = checkUserNameLength(MIN_NAME_LENGTH);
  </script>
</body>

40.11 커스텀 이벤트

40.11.1 커스텀 이벤트 생성

이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 이벤트 타입을 지정해서 커스텀 이벤트로 생성할 수 있다.

// KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
const keyboardEvent = new KeyboardEvent('keyup');
console.log(keyboardEvent.type); // keyup

// 임의의 문자열로 새로운 이벤트 타입을 지정하는 경우 CustomEvent 생성자 함수를 사용한다.
const customEvent = new CustomEvent('custom');
console.log(customEvent.type); // custom

생성된 커스텀 이벤트 객체는 bubbles와 cancelable 프로퍼티의 기본값이 false다. 이를 true로 설정하려면 생성자 함수의 두 번째 인수로 bubble나 cancelable 프로퍼티를 갖는 객체를 전달해야 한다.

const customEvent = new MouseEvent('click',{
	bubbles: true,
  	cancelable: true
});

또한, 커스텀 이벤트 객체는 이벤트 타입에 따라 가지는 이벤트 고유의 프로퍼티 값을 지정할 수 있다.

const customEvent = new MouseEvent('click',{
	bubbles: true,
  	cancelable: true,
  	clientX: 50,
});

40.11.2 커스텀 이벤트 디스패치

생성된 커스텀 이벤트는 dispatchEvent 메서드로 이벤트를 발생시킬수 있다.
dispatchEvent 메서드는 이벤트 핸들러를 동기 처리 방식으로 호출하기 때문에 해당 메서드로 이벤트를 디스패치하기 이전에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록해야 한다.

<body>
  <button class="btn">click</button>
  <script>
  	const $btn = document.querySelector('.btn');
    
    // 버튼 요소에 custom 커스템 이벤트 핸들러를 등록
    // 커스텀 이벤트 디스패치 전에 이벤트 핸들러를 등록해야 한다.
    $btn.addEventListener('custom', e => {
    	// e.detail에는 CustomEvent 함수의 두 번째 인수로 전달할 정보가 담겨 있다.
    	alert(e.detail.message);
    })
    
    const customEvent = new CustomEvent('custom',{
    	detail: { message: 'Hello' } // 이벤트와 함께 전달하고 싶은 정보
    })
    
    // 커스텀 이벤트 디스패치
    $btn.dispatchEvent(customEvent);
  </script>
</body>
profile
깊이 있는 학습, 클린 코드, 의사소통

0개의 댓글