일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- react customHook 예시
- 프로그래머스 데브코스 프론트엔드
- 모던 자바스크립트 TIL
- 프로그래머스 데브코스
- 모던 javascript Deep Dive
- useRef 지역 변수
- 개발자 특강
- TypeScript 문법 소개
- 머쓱이
- 백준 js
- 프로그래머스 K_Digital Training
- Vue3
- KDT 프로그래머스
- 프로그래머스 데브코스 프론트엔드 TIL
- 모던 자바스크립트 딥다이브
- 우테캠 회고록
- react 프로젝트 리팩토링
- 인프런 자바스크립트 알고리즘 문제풀이
- 모던 자바스크립트 Deep Dive
- 프로그래머스 K_Digital Training 프론트엔드
- 투포인터알고리즘 js
- 모던 자바스크립트 Deep Dive TIL
- Frontend Roadmap
- 백준 node.js
- useEffect return
- frontend roadmap study
- 리팩토링 회고
- KDT 프로그래머스 데브코스 프론트엔드
- K_Digital Training
- Vue3 Router
- Today
- Total
프론트엔드 개발자의 기록 공간
[JavaScript DeepDive] 40장_이벤트 본문
브라우저는 처리해야 할 특정 사건이 발생하면 이를 감지하여 이벤트를 발생 시킨다.
예를 들어, 클릭, 입력 등이 일어나면 브라우저는 이를 감지하여 특정한 타입의 이벤트를 발생시킨다.
특정 타입의 이벤트에 대해 반응하여 어떤 일을 하고 싶다면 해당하는 타입의 이벤트가 발생했을 때
호출될 함수를 브라우저에게 알려 호출을 위임한다. 이때 이벤트가 발생했을 때 호출될 함수를
이벤트 핸들러라 하고, 이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을
이벤트 핸들러 등록이라 한다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 사용자가 버튼을 클릭하면 함수를 호출하도록 요청
$button.onclick = () => { alert('button click'); };
</script>
</body>
</html>
이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용 할 수 있다. 이와 같이 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍
이라 한다.
이벤트 타입은 이벤트의 종류를 나타낸다. 타입에는 약 200여 가지가 있다. 사용 빈도가 높은 이벤트의 목록은 마우스 이벤트, 키보드 이벤트, 포커스 이벤트, 폼 이벤트, 값 변경 이벤트 등의 이벤트 타입이 있지만 소개하진 않을 예정이다. 이벤트 타입에 대한 상세 목록은 MDN의 Event reference에서 확인할 수 있다.
✍ 이벤트 핸들러 등록
이벤트 핸들러는 이벤트가 발생했을 때 브라우저에 호출을 위임한 함수다.
이벤트 핸들러를 등록하는 방법은 3가지다.
📌 이벤트 핸들러 어트리뷰트 방식
이벤트 핸들러 어트리뷰트 값으로 함수 호출문 등의 문을 할당하면 이벤트 핸들러가 등록된다.
<!DOCTYPE html>
<html>
<body>
<button onclick="sayHi('Lee')">Click me!</button>
<script>
function sayHi(name) {
console.log(`Hi! ${name}.`);
}
</script>
</body>
</html>
📌 이벤트 핸들러 프로퍼티 방식
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티에 이벤트 핸들러를 바인딩
$button.onclick = function () {
console.log('button click');
};
</script>
</body>
</html>
📌 addEventListener 메서드 방식
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
// $button.onclick = function () {
// console.log('button click');
// };
// addEventListener 메서드 방식
$button.addEventListener('click', function () {
console.log('button click');
});
</script>
</body>
</html>
이벤트 핸들러 프로퍼티 방식 ( $button.onclick = function () {} )은 이벤트 핸들러 프로퍼티에 이벤트핸들러를 바인딩하지만 addEventListener 메서드에는 이벤트 핸들러를 인수로 전달한다. 그래서 두 개의 방식으로 이벤트 핸들러를 사용하면 중복되는 문제가 생긴다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function () {
console.log('[이벤트 핸들러 프로퍼티 방식]button click');
};
// addEventListener 메서드 방식
$button.addEventListener('click', function () {
console.log('[addEventListener 메서드 방식]button click');
});
</script>
</body>
</html>
버튼 요소 클릭 시 "[이벤트 핸들러 프로퍼티 방식]button click" "
[addEventListener 메서드 방식]button click" 두 개 전부 출력
addEventListener 메서드 방식은 이벤트 핸들러 프로퍼티에 바인딩된 이벤트 핸들러에 아무런 영향을 주지 않는다. 따라서 버튼 클릭 이벤트가 발생하면 2개의 이벤트 핸들러가 모두 호출된다.
✍ 이벤트 핸들러 제거
addEventListener 메서드로 등록한 이벤트 핸들러를 제거하려면 EventTarget.prototype.removeEventListener 메서드를 사용한다. 이때 등록한 메서드와 삭제 메서드의 인수가 동일해야 한다.
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');
const handleClick = () => console.log('button click');
// 이벤트 핸들러 등록
$button.addEventListener('click', handleClick);
// 이벤트 핸들러 제거
// addEventListener 메서드에 전달한 인수와 removeEventListener 메서드에
// 전달한 인수가 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.
$button.removeEventListener('click', handleClick, true); // 실패
$button.removeEventListener('click', handleClick); // 성공
</script>
</body>
</html>
무명 함수를 이벤트 핸들러로 등록한 경우 참조할 수 있는 방법이 없기 때문에 제거 할 수 없다.
따라서 이벤트 핸들러의 참조를 변수나 자료구조에 저장하고 있어야 한다.
// 이벤트 핸들러 등록
$button.addEventListener('click', () => console.log('button click'));
// 등록한 이벤트 핸들러를 참조할 수 없으므로 제거할 수 없다.
기명 이벤트 핸들러 내부에서 removeEventListener 메서드를 호출하여 이벤트 핸들러를 제거하는 것은
가능하다. 대신 이벤트 핸들러는 단 한번만 호출된다. 여러 번 클릭해도 단 한 번만 이벤트가 호출되고 삭제 된다.
✍ 이벤트 객체
이벤트가 발생하면 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다.
생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
이벤트 핸들러 프로퍼티 방식
<!DOCTYPE html>
<html>
<body>
<p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
const $msg = document.querySelector('.message');
// 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
function showCoords(e) {
$msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
}
document.onclick = showCoords;
</script>
</body>
</html>
클릭 이벤트 발생 시 이벤트 객체가 동적으로 생성되고 그 객체가 이벤트 핸들러의 첫 번째 인수로 전달 받을 수 있다. "e" 라는 매개변수를 선언했으나 다른 이름을 사용하여도 상관없다.
아마 프론트 개발을 하신 분들이라면 다음과 같은 코드를 많이 사용했을 것이다 "e.preventDefault()"
이런 것들이 전부 이벤트 객체에 해당하는 것들이다.
이벤트 핸들러 어트리뷰트 방식
<!DOCTYPE html>
<html>
<head>
<style>
html, body { height: 100%; }
</style>
</head>
<!-- 이벤트 핸들러 어트리뷰트 방식의 경우 event가 아닌 다른 이름으로는 이벤트 객체를
전달받지 못한다. -->
<body onclick="showCoords(event)">
<p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
const $msg = document.querySelector('.message');
// 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
function showCoords(e) {
$msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
}
</script>
</body>
</html>
이벤트 핸드러 어트리뷰트 방식의 경우 이벤트 객체를 전달받으려면 이벤트 핸들러의 첫 번째 매개변수
이름이 반드시 event이어야 한다.
📌 이벤트 객체의 상속 구조
이벤트가 발생하면 이벤트 타입에 따라 다양한 타입의 이벤트 객체가 생성된다. 이벤트 객체는 다음과 같은 상속 구조를 갖는다.
✍ 이벤트 전파
DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다. 이를 이벤트 전파라고 한다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</body>
</html>
ul 요소의 두 번째 자식 요소인 li 요소를 클릭하면 클릭 이벤트가 발생한다고 가정하자. 클릭 시
생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타킷 중심으로 DOM 트리를 통해 전파된다.
이벤트 전파는 이벤트 객체가 전파되는 방향에 따라 3단계로 구분할 수 있다.
- 캡처링 단계 : 이벤트가 상위 요소에서 하위 요소 방향으로 전파
- 타깃 단계 : 이벤트가 이벤트 타깃에 도달
- 버블링 단계 : 이벤트가 하위 요소에서 상위 요소 방향으로 전파
예를 들어, ul 요소에 이벤트 핸들러를 바인딩하고 ul 하위 요소인 li 요소를 클릭해보자.
이때 이벤트 타켓(event.target) 이벤트가 발생한곳은 li 요소이고 커런트 타깃(event.currentTarget) 즉, 이벤트 핸들러를 바인딩 한 ul 요소다.
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');
// #fruits 요소의 하위 요소인 li 요소를 클릭한 경우
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
});
</script>
</body>
</html>
✍ 이벤트 위임
예를들어 ul 태그 하위에 5개의 li 태그가 있다고 가정하자. li 요소를 클릭하면 선택된 li에 특정 동작을 수행하고 나머지 li 태그의 상태값을 바꾼다고 가정하자.
그러면 li의 각각의 요소에 이벤트 핸들러를 등록해야한다. 만일 li 태그가 100개라면 이벤트 핸들러로 100개를 등록해야한다. DOM 요소에 많은 이벤트 핸들러를 등록하므로 성능 저하의 원인이 될뿐더러 유지보수에도 부적합한 코드를 생상하게 한다.
이벤트 위임은 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다. (위의 가정에서는 ul 태그에만 등록한다.)
이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러를 등록하면 여러 개의 하위 요소에 등록할 필요가 없다. 또한 동적으로 하위 DOM 요소를 추가하더라도 문제가 없다.
이벤트 위임을 통해 하위 DOM 요소에서 발생한 이벤트를 처리할 때 주의할 점은 상위 요소에
이벤트 핸들러를 등록하기 때문에 이벤트 타킷, 즉 이벤트를 실제로 발생시킨 DOM 요소가 개발자가
기대한 DOM 요소가 아닐 수도 있다. ul 태그 자체 일 수도 있고, ul 태그 하위에 있는 li를 제외한 다른 요소 일 수도 있다. 따라서 이벤트 타깃을 제대로 검사해야 한다.
function activate({ target }) {
// 이벤트를 발생시킨 요소(target)이 ul#fruits의 자식 요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;
...
Element.prototype.matches 메서드를 인수로 전달된 선택자에 의해 특정 노드를 탐색 가능한지 확인한다.
✍ DOM 요소의 기본 동작 조작
📌 DOM 요소의 기본 동작 중단
DOM 요소는 저마다 기본 동작이 있다. 예를 들어, a 요소를 클릭하면 href 어트리뷰트에 지정된 링크로
이동하고, checkbox 또는 radio 요소를 클릭하면 체크 또는 해제된다.
이벤트 객체의 preventDefault 메서드는 이러한 DOM 요소의 기본 동작을 중단시킨다.
<!DOCTYPE html>
<html>
<body>
<a href="https://www.google.com">go</a>
<input type="checkbox">
<script>
document.querySelector('a').onclick = e => {
// a 요소의 기본 동작을 중단한다.
e.preventDefault();
};
document.querySelector('input[type=checkbox]').onclick = e => {
// checkbox 요소의 기본 동작을 중단한다.
e.preventDefault();
};
</script>
</body>
</html>
📌 이벤트 전파 방지
이벤트 객체의 stopPropagation 메서드는 이벤트 전파를 중지시킨다.
<!DOCTYPE html>
<html>
<body>
<div class="container">
<button class="btn1">Button 1</button>
<button class="btn2">Button 2</button>
<button class="btn3">Button 3</button>
</div>
<script>
// 이벤트 위임. 클릭된 하위 버튼 요소의 color를 변경한다.
document.querySelector('.container').onclick = ({ target }) => {
if (!target.matches('.container > button')) return;
target.style.color = 'red';
};
// .btn2 요소는 이벤트를 전파하지 않으므로 상위 요소에서 이벤트를 캐치할 수 없다.
document.querySelector('.btn2').onclick = e => {
e.stopPropagation(); // 이벤트 전파 중단
e.target.style.color = 'blue';
};
</script>
</body>
</html>
위 예제에서 상위 DOM 요소 container 요소에 이벤트를 위임했다. 따라서 하위 DOM 요소에서 클릭 이벤트 발생시 상위에서 이벤트를 캐치하여 처리한다. 하지만 하위 요소 중에서 btn2 요소는 자신이 발생시킨 이벤트가 전파되는 것을 중단하여 자신에게 바인딩된 이벤트 핸들러만 실행되도록 한다.
이처럼 stopPropagation 메서드는 하위 DOM 요소의 이벤트를 개별적으로 처리하기 위해 이벤트의 전파를 중단시킨다.
✍ 이벤트 핸들러 내부의 this
📌 이벤트 핸들러 어트리뷰트 방식
이벤트 핸들러 어트리뷰트 방식으로 선언된 함수 내부의 this는 전역 객체 window를 가리킨다.
<!DOCTYPE html>
<html>
<body>
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
console.log(this); // window
}
</script>
</body>
</html>
handleClick 함수는 이벤트 핸들러에 의해 일반 함수로 호출된다.
일반 함수로 호출된 함수내부의 this는 전역 객체를 가리킨다. 따라서 this는 전역 객체 window를 가리킨다.
📌 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는
이벤트를 바인딩한 DOM 요소를 카리킨다. 즉, 이벤트 핸들러 내부의 this는 이벤트 객체의
currentTarget 프로퍼티와 같다.
<!DOCTYPE html>
<html>
<body>
<button class="btn1">0</button>
<button class="btn2">0</button>
<script>
const $button1 = document.querySelector('.btn1');
const $button2 = document.querySelector('.btn2');
// 이벤트 핸들러 프로퍼티 방식
$button1.onclick = function (e) {
// this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
console.log(this); // $button1
console.log(e.currentTarget); // $button1
console.log(this === e.currentTarget); // true
// $button1의 textContent를 1 증가시킨다.
++this.textContent;
};
// addEventListener 메서드 방식
$button2.addEventListener('click', function (e) {
// this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
console.log(this); // $button2
console.log(e.currentTarget); // $button2
console.log(this === e.currentTarget); // true
// $button2의 textContent를 1 증가시킨다.
++this.textContent;
});
</script>
</body>
</html>
화살표 함수로 종의한 이벤트 핸들러 내부의 this는 상위 스코프의 this를 가리킨다.
화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다.
✍ 커스텀 이벤트
📌 커스텀 이벤트 생성
이벤트 객체는 Event, UIEvent, MouseEvent 같은 이벤트 생성자 함수로 생성할 수 있다.
이벤트가 발생하면 암묵적으로 생성되는 이벤트 객체는 발생한 이벤트의 종류에 따라 이벤트 타입이
결정된다. 하지만 Event, UIEvent, MouseEvent 같은 이벤트 생성자 함수를 호출하여 명시적으로 생성한 이벤트 객체는 임의의 이벤트 타입을 지정할 수 있다. 이처럼 개발자의 의도로 생성된 이벤트를 커스텀 이벤트라 한다.
// KeyboardEvent 생성자 함수로 keyup 이벤트 타입의 커스텀 이벤트 객체를 생성
const keyboardEvent = new KeyboardEvent('keyup');
console.log(keyboardEvent.type); // keyup
// CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new CustomEvent('foo');
console.log(customEvent.type); // foo
생성된 커스텀 이벤트 객체는 버블링되지 않으며 preventDefault 메서드로 취소할 수도 없다.
즉, 커스텀 이벤트 객체는 bubbbles와 cancelable 프로퍼티의 값이 false로 기본 설정된다.
// MouseEvent 생성자 함수로 click 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new MouseEvent('click');
console.log(customEvent.type); // click
console.log(customEvent.bubbles); // false
console.log(customEvent.cancelable); // false
생성된 커스텀 이벤트는 dispatchEvent 메서드로 디스패치(이벤트를 발생시키는 행위)할 수 있다.
dispatchEvent 메서드에 이벤트 객체를 인수로 전달하면서 호출하면 인수로 전달한 이벤트 타입의 이벤트가 발생한다.
활용 예시 코드
<!DOCTYPE html>
<html>
<body>
<button class="btn">Click me</button>
<script>
const $button = document.querySelector('.btn');
// 버튼 요소에 foo 커스텀 이벤트 핸들러를 등록
// 커스텀 이벤트를 디스패치하기 이전에 이벤트 핸들러를 등록해야 한다.
$button.addEventListener('foo', e => {
// e.detail에는 CustomEvent 함수의 두 번째 인수로 전달한 정보가 담겨 있다.
alert(e.detail.message);
});
// CustomEvent 생성자 함수로 foo 이벤트 타입의 커스텀 이벤트 객체를 생성
const customEvent = new CustomEvent('foo', {
detail: { message: 'Hello' } // 이벤트와 함께 전달하고 싶은 정보
});
// 커스텀 이벤트 디스패치. click 이벤트가 발생
$button.dispatchEvent(customEvent);
</script>
</body>
</html>
👨💻 이번 장도 내용이 방대했다. 커스텀 이벤트를 직접 만들고 활용하는 방식은 아직 생소해서 좀 더 봐야 할 것 같다.
이번 장에서 중요한 것은 이벤트 캡처링, 버블링과 이벤트 위임으로 활용할 수 있는 예제가 가장 중요한 것 같다.
개발을 하다 보면 이벤트 요소를 다루는 경우가 많은데 예상치 못한 문제가 발생한 적이 있기 때문에 잘 볼 필요가 있다.
'모던 자바스크립트 Deep Dive' 카테고리의 다른 글
[JavaScript DeepDive] 42장_비동기 프로그래밍 (0) | 2022.03.09 |
---|---|
[JavaScript DeepDive] 41장_타이머 (0) | 2022.03.08 |
[JavaScript DeepDive] 39장_DOM (0) | 2022.03.07 |
[JavaScript DeepDive] 38장_브라우저의 렌더링 과정 (0) | 2022.03.06 |
[JavaScript DeepDive] 37장_Set과 Map (0) | 2022.03.05 |