`target`과 `currentTarget`의 차이점을 공부하면서 Event에 대해서 다시 공부하게 되었습니다.

1. 이벤트(Event)란?
이벤트(event)는 어떤 사건을 의미한다.
브라우저에서의 이벤트란 예를 들어 사용자가 버튼을 클릭했을 때, 웹페이지가 로드되었을 때와 같은 것인데 이것은 DOM 요소와 관련이 있다.
이벤트가 발생하는 시점이나 순서를 사전에 인지할 수 없으므로 일반적인 제어 흐름과는 다른 접근 방식이 필요하다. 즉, 이벤트가 발생하면 누군가 이를 감지할 수 있어야 하며 그에 대응하는 처리를 호출해 주어야 한다.
브라우저는 이벤트를 감지할 수 있으며 이벤트 발생 시에는 통지해 준다. 이 과정을 통해 사용자와 웹페이지는 상호작용(Interaction)이 가능하게 된다.
2. 이벤트 핸들러란?
이벤트에 연결되어서 이벤트에 대응하는 처리를 기술하는 함수를 의미한다.
이벤트가 발생하기 전에는 실행되지 않다가 이벤트가 발생하면 그에 맞는 반응을 해준다.
<!DOCTYPE html>
<html>
<body>
<button class="myButton">Click me!</button>
<script>
document.querySelector('.myButton').addEventListener('click', function () {
alert('Clicked!');
});
</script>
</body>
</html>
3. 이벤트 루프(Event Loop)와 동시성(Concurrency)
브라우저는 단일 쓰레드(single-thread)에서 이벤트 드리븐(event-driven) 방식으로 동작한다.
단일 쓰레드는 쓰레드가 하나뿐이라는 의미이며 이말은 곧 하나의 작업(task)만을 처리할 수 있다는 것을 의미한다. 하지만 실제로 동작하는 웹 애플리케이션은 많은 task가 동시에 처리되는 것처럼 느껴진다. 이처럼 자바스크립트의 동시성(Concurrency)을 지원하는 것이 바로 이벤트 루프(Event Loop)이다.
브라우저의 환경을 그림으로 표현하면 아래와 같다.

구글의 V8을 비롯한 대부분의 자바스크립트 엔진은 크게 2개의 영역으로 나뉜다.(callstack, heap)
Call Stack(호출 스택)
작업이 요청되면(함수가 호출되면) 요청된 작업은 순차적으로 Call Stack에 쌓이게 되고 순차적으로 실행된다. 자바스크립트는 단 하나의 Call Stack을 사용하기 때문에 해당 task가 종료하기 전까지는 다른 어떤 task도 수행될 수 없다.
Heap
동적으로 생성된 객체 인스턴스가 할당되는 영역이다.
이와 같이 자바스크립트 엔진은 단순히 작업이 요청되면 Call Stack을 사용하여 요청된 작업을 순차적으로 실행할 뿐이다. 앞에서 언급한 동시성(Concurrency)을 지원하기 위해 필요한 비동기 요청(이벤트를 포함) 처리는 자바스크립트 엔진을 구동하는 환경 즉 브라우저(또는 Node.js)가 담당한다.
Event Queue(Task Queue)
비동기 처리 함수의 콜백 함수, 비동기식 이벤트 핸들러, Timer 함수(setTimeout(), setInterval())의 콜백 함수가 보관되는 영역으로 이벤트 루프(Event Loop)에 의해 특정 시점(Call Stack이 비어졌을 때)에 순차적으로 Call Stack으로 이동되어 실행된다. MicroTask Queue와 MacroTask Queue로 나뉜다.
▶이벤트 발생 시 실행되어야 할 callback 함수가 추가되는 공간
Event Loop(이벤트 루프)
Call Stack 내에서 현재 실행중인 task가 있는지 그리고 Event Queue에 task가 있는지 반복하여 확인한다. 만약 Call Stack이 비어있다면 Event Queue 내의 task가 Call Stack으로 이동하고 실행된다.

*DOM 이벤트 핸들러도 Macrotask queue에 속한다.
TASK Queue의 실행 순서에 대해서 더 알아보고자 한다면 이 글을 참조하자.
4. 이벤트의 종류(UI, Keyboard, Mouse, Drag and Drop, Form, Focus, Clipboard, CSS)
브라우저에는 많은 이벤트 종류가 있다.( Event reference )
그 중 대표적인 이벤트 종류를 알아보자.
단,
- 버블링되는 이벤트 타입이 있고 버블링되지 않는 이벤트도 있으므로 주의하자.
- 또한 자식 요소까지 이벤트의 발생 (추적) 범위로 삼는 이벤트도 있으니 주의하자. (예를 들어 유저 인터렉션이 nested된 자식 요소에서 발생했는데 자식 요소도 자신의 감시 구역으로 따지기 때문에 이벤트가 발생하는 경우가 있다.)
UI Event
| Event | Description |
| load | 웹페이지의 로드가 완료되었을 때 |
| unload | 웹페이지가 언로드될 때(주로 새로운 페이지를 요청한 경우) |
| error | 브라우저가 자바스크립트 오류를 만났거나 요청한 자원이 존재하지 않는 경우 |
| resize | 브라우저 창의 크기를 조절했을 때 |
| scroll | 사용자가 페이지를 위아래로 스크롤할 때 |
| select | 텍스트를 선택했을 때 |
| DOMContentLoaded | HTML이 전부 로드 및 처리되어 DOM 생성이 완료됐을 때 발생 |
Keyboard Event
`keydown` > `keypress` > `keyup` 순서로 발생한다.
| Event | Description |
| keydown | 키를 누른 순간 |
| keypress | 키를 누른 순간(입력으로 인해 출력값이 변하는 키 입력일 경우에만 발생. e.g. Backspace, ESC 등의 경우 발생 X) |
| keyup | 누르고 있던 키를 뗄 때. 입력 글자 수(length)를 여기서 확인할 수 있다. |
-한글 입력을 캐치하지 못하는 keypress
`keypress`는 한글 입력을 캐치하지 못한다.
그래서 한글과 같은 조합형 문자를 현재 입력해서 문자를 완성하는 와중에 핸들링을 하고 싶다면 `keydown`을 사용해야 한다.
- event.isComposing
`Keyboard Event`객체에는 `isComposing`이라는 프로퍼티가 있다.
이 프로퍼티는 한글과 같은 조합형 문자를 입력받을 때를 대비해서 조합 문자를 입력 중인지 여부를 boolean으로 나타낸다.
즉, 현재 작성중인 (혹은 작성한) 문자가 조합형 문자인지를 나타내고 이를 이용해서 영문과 구분할 수 있다.
한글을 입력하다보면 한글 아래에 '_'가 표시되는 경우가 있는데 이 과정이 compose 과정이다.

글자를 완성시키려고 할 때 아래에 '_'가 있으면 `isComposing`은 `true`인 것이다.
단, 현재 입력하는 문자가 `ㅁ`인지 `a`인지 알 수 없으므로 즉, 조합문자 여부를 알 수 없으므로 자/모음만 따로 있으면 `keydown`은 `isComposing`을 `false`로 판단한다. 반면 `keyup`은 입력이 끝났기 때문에 `isComposing`이 `false`인지 `true`인지 즉, compose 문자인지 아닌지 정확하게 판단할 수 있으므로 자/모음만 따로 입력되도 `true`를 반환해준다.
이러한 isComposing 프로퍼티는 `keydown`이벤트에서 유용하게 사용될 수 있다.
- keypress 이벤트의 장점
`keydown` 이벤트는 한글과 같은 조합형 문자를 사용하면 Enter 키를 눌렀을 때 이벤트가 불필요하게 중복되서 발생하는 문제가 있다. 한글 글자를 입력 후 Enter했을 때 `keydown`이벤트가 먼저 `isComposing: false`로 처리되고, 또 다시 `isComposing: true`상태로 또 처리된다.
반면, `keypress`이벤트를 사용하면 조합형 문자 입력 후 Enter 키 입력 시 이벤트의 (`keypress`이벤트의)중복없이 처리가 가능하다. 한 번만 발생한다는 뜻이다.
그럼에도 불구하고 한글과 같은 조합형 문자를 Enter할 때 `keydown`을 써야 한다면, 이벤트 핸들러 내부에서 조건문으로`isComposing` 속성이 `false`인 상태에서만 처리하도록 하면 중복 처리 없이 가능하다. `true` 상태가 아니라 `false`인 상태를 조건으로 주는 이유는 `isComposing: false`가 `true`보다 먼저 발생하기 때문이다.
function keydownHandler(e) {
if(e.type === 'keydown'&& !e.isComposing && e.key === 'Enter' && !e.shiftKey) {
sendMyText();
e.preventDefault();
}
}
input.addEventListener('keydown', keydownHandler);
아래는 키보드 이벤트에 따른 결과를 콘솔로 볼 수 있는 코드다.
See the Pen KeyboarEvent study by Olimjo (@OLIMJO) on CodePen.
Mouse Event
| Event | Description |
| click | 마우스 버튼을 클릭했을 때 |
| contextmenu | 요소 위에서 마우스 오른쪽 버튼을 눌렀을 때 발생 |
| dblclick | 마우스 버튼을 더블 클릭했을 때 |
| mousedown | 마우스 버튼을 누르고 있을 때 |
| mouseup | 누르고 있던 마우스 버튼을 뗄 때 |
| mousemove | 마우스를 움직일 때 (터치스크린에서 동작하지 않는다) |
| mouseover | 마우스를 요소 위로 (처음) 움직였을 때 (터치스크린에서 동작하지 않는다.) 버블링 한다. 자식 요소의 영역까지 계산한다. 자식 요소의 영역까지 계산한다는 것은 자식 요소로 들어갈 때도 이벤트가 발생한다는 것이다. |
| mouseenter | 마우스를 요소 위로 (처음) 움직였을 때 (터치스크린에서 동작하지 않는다. 버블링 하지 않는다.) |
| mouseout | 마우스를 요소 밖으로 움직였을 때 (터치스크린에서 동작하지 않는다. 버블링 한다. 자식 요소의 영역까지 계산한다. 자식 요소의 영역까지 계산한다는 것은 자식 요소에서 빠져 나올 때도 이벤트가 발생한다는 것이다. |
| mouseleave | 마우스를 요소 밖으로 움직였을 때(터치스크린에서 동작하지 않는다. 버블링 하지 않는다.) |
Drag and Drop Event
| Event | Description |
| dragenter | 드래그한 요소나 텍스트 블록을 적합한 드롭 대상 위에 올렸을 때 발생(드롭할 위치에 입장[도달]했을 때 맨 처음 한 번만 발생) |
| dragover | 요소나 텍스트 블록이 적합한 드롭 대상 위를 지나갈 때 발생(매 수백 밀리초마다 발생) |
| dragleave | 드래그하는 요소나 텍스트 블록이 적합한 드롭 대상에서 벗어났을 때 발생 |
| drop | 요소나 텍스트 블록이 적합한 드롭 대상에 드롭했을 때 발생. |
Focus Event
| Event | Description |
| focus | 요소가 포커스를 얻었을 때(버블링이 일어나지 않음) |
| focusin | 요소가 포커스를 얻었을 때 |
| blur | 요소가 포커스를 잃었을 때(버블링이 일어나지 않음) |
| focusout | 요소로부터 포커스가 빠져나가는 순간 |
Form Event (input event)
| Event | Description |
| input | input 또는 textarea 요소의 값이 변경되었을 때. 즉, 사용자가 입력을 할 때 발생 |
| change | input 값(내용)이 이전과 비교했을 때 바뀌면 발생. (요소의 내용 혹은 상태가 전후랑 동일하면 발생하지 않음) input 태그의 type에 따라 발생 시점이 다름. 보통 값을 입력하는 텍스트 입력하는 type들의 경우에는 포커스 아웃이나 Enter 키 입력 때 발생하고, 나머지의 경우에는 값이 바뀐 후마다 바로 발생한다고 보면 됨. 어쨌든 중요한 점은 이전값과 달라야 발생한다는 점임. |
| submit | form을 submit할 때 (버튼 또는 키) |
| reset | reset 버튼을 클릭할 때 (최근에는 사용 안함) |
| select | 입력 양식의 하나가 선택되는 순간 |
Clipboard Event
| Event | Description |
| cut | 콘텐츠를 잘라내기할 때 |
| copy | 콘텐츠를 복사할 때 |
| paste | 콘텐츠를 붙여넣기할 때 |
CSS Event
| Event | Description |
| transitioned | CSS 애니메이션이 종료되었을 때 발생 |
5. 이벤트 핸들러 등록
이벤트가 발생했을 때 동작할 이벤트 핸들러를 이벤트에 등록하는 방법은 아래와 같이 3가지이다.
5.1 인라인 이벤트 핸들러 방식
<!DOCTYPE html>
<html>
<body>
<button onclick="myHandler()">Click me</button>
<script>
function myHandler() {
alert('Button clicked!');
}
</script>
</body>
</html>
이 방식은 더 이상 사용되지 않으며 사용해서도 안 된다. 오래된 코드에서 간혹 이 방식을 사용한 것이 있기 때문에 알아둘 필요는 있다. HTML과 Javascript는 관심사가 다르므로 분리하는 것이 좋다.
5.2 이벤트 핸들러 프로퍼티 방식
인라인 이벤트 핸들러 방식처럼 HTML과 Javascript가 뒤섞이는 문제는 해결할 수 있는 방식이다. 하지만 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만을 바인딩할 수 있다는 단점이 있다.
<!DOCTYPE html>
<html>
<body>
<button class="btn">Click me</button>
<script>
const btn = document.querySelector('.btn');
// 이벤트 핸들러 프로퍼티 방식은 이벤트에 하나의 이벤트 핸들러만을 바인딩할 수 있다
// 첫번째 바인딩된 이벤트 핸들러 => 실행되지 않는다.
btn.onclick = function () {
alert('① Button clicked 1');
};
// 두번째 바인딩된 이벤트 핸들러
btn.onclick = function () {
alert('① Button clicked 2');
};
// addEventListener 메소드 방식
// 첫번째 바인딩된 이벤트 핸들러
btn.addEventListener('click', function () {
alert('② Button clicked 1');
});
// 두번째 바인딩된 이벤트 핸들러
btn.addEventListener('click', function () {
alert('② Button clicked 2');
});
</script>
</body>
</html>
① Button clicked 2 > ② Button clicked 1 > ② Button clicked 2 순서로 경고창을 띄워준다.
5.3 addEventListener 메소드 방식
`addEventListener` 메소드를 이용하여 대상 DOM 요소에 이벤트를 바인딩하고 해당 이벤트가 발생했을 때 실행될 콜백 함수(이벤트 핸들러)를 지정한다.
▶헷갈리지 말자! addEventListener는 메소드다. 이 메소드가 받는 콜백 함수가 이벤트 핸들러다.

addEventListener 함수 방식은 이전 방식에 비해 아래와 같이 보다 나은 장점을 갖는다.
- 하나의 이벤트에 대해 하나 이상의 이벤트 핸들러를 추가할 수 있다.
- 캡처링과 버블링을 지원한다.
- HTML 요소뿐만아니라 모든 DOM 요소(HTML, XML, SVG)에 대해 동작한다. 브라우저는 웹 문서(HTML, XML, SVG)를 로드한 후, 파싱하여 DOM을 생성한다.
<!DOCTYPE html>
<html>
<body>
<script>
addEventListener('click', function () {
alert('Clicked!');
});
</script>
</body>
</html>
위와 같이 대상 DOM 요소(target)를 지정하지 않으면 전역객체 window, 즉 DOM 문서를 포함한 브라우저의 윈도우에서 발생하는 click 이벤트에 이벤트 핸들러를 바인딩한다. 따라서 브라우저 윈도우 어디를 클릭하여도 이벤트 핸들러가 동작한다.
... 중간 생략했음...
사용 옵션에 대해서 전부 모아서 정리하자면,

capture - 기본값 false
capturing(true)을 캐치할 것인지 bubbling(false)을 캐치할 것인지 여부.
기본적으로 bubbling을 캐치함.
once - 기본값 false
동일한 event type에 대한 listen을 한 번만 수행할 것인지 여부
passive - 기본값 true
passive는 브라우저에게 이 이벤트가 `preventDefault()`를 사용하였는지에 대해 알리는 역할을 함.
기본값은 `true`로 기본 패시브를 장착함을 의미. preventDefault가 아님을 의미함.
signal
6. 이벤트 핸들러 함수 내부의 this
6.1 인라인 이벤트 핸들러 방식
인라인 이벤트 핸들러 방식의 경우, 이벤트 핸들러는 일반 함수로서 호출되므로 이벤트 핸들러 내부의 `this`는 전역 객체 `window`를 가리킨다.
<!DOCTYPE html>
<html>
<body>
<button onclick="foo()">Button</button>
<script>
function foo () {
console.log(this); // window
}
</script>
</body>
</html>
6.2 이벤트 핸들러 프로퍼티 방식
이벤트 핸들러 프로퍼티 방식에서 이벤트 핸들러는 메소드이므로 이벤트 핸들러 내부의 `this`는 이벤트에 바인딩된 요소를 가리킨다. 이것은 이벤트 객체의 `currentTarget` 프로퍼티와 같다.
<!DOCTYPE html>
<html>
<body>
<button class="btn">Button</button>
<script>
const btn = document.querySelector('.btn');
btn.onclick = function (e) {
console.log(this); // <button id="btn">Button</button>
console.log(e.currentTarget); // <button id="btn">Button</button>
console.log(this === e.currentTarget); // true
};
</script>
</body>
</html>
6.3 ⭐addEventListener 메소드 방식(🚨함정 조심)⭐
addEventListener 메소드에서 지정한 이벤트 핸들러는 콜백 함수이지만 이벤트 핸들러 내부의 this는 이벤트 리스너에 바인딩된 요소(currentTarget)를 가리킨다. 이것은 이벤트 객체의 `currentTarget` 프로퍼티와 같다.
▶ (암시적 바인딩이 사용되지 않은) 보통 콜백함수의 this는 전역 객체를 가리키기 마련인데, 이벤트 리스너의 콜백함수인 이벤트 핸들러는 addEventListener 메소드(이벤트 리스너)가 가리키고 있는 프로토타입 수준에서 `currentTarget`을 바인딩 해놓았기 때문에 같은 `currentTarget`을 가리키게 된다.
<!DOCTYPE html>
<html>
<body>
<button class="btn">Button</button>
<script>
const btn = document.querySelector('.btn');
btn.addEventListener('click', function (e) {
console.log(this); // <button id="btn">Button</button>
console.log(e.currentTarget); // <button id="btn">Button</button>
console.log(this === e.currentTarget); // true
});
</script>
</body>
</html>
7. 이벤트의 흐름(버블링과 캡처링)
계층적 구조에 포함되어 있는 HTML 요소에 이벤트가 발생할 경우 연쇄적 반응이 일어난다. 즉, 이벤트가 전파(Event Propagation)되는데 전파 방향에 따라 버블링(Event Bubbling)과 캡처링(Event Capturing)으로 구분할 수 있다.
자식 요소에서 발생한 이벤트가 부모 요소로 전파되는 것을 버블링이라 하고, 자식 요소에서 발생한 이벤트가 부모 요소부터 시작하여 이벤트를 발생시킨 자식 요소까지 도달하는 것을 캡처링이라 한다. 주의할 것은 버블링과 캡처링은 둘 중에 하나만 발생하는 것이 아니라 캡처링부터 시작하여 버블링으로 종료한다는 것이다. 즉, 이벤트가 발생했을 때 캡처링과 버블링은 기본적으로 순차적으로 발생한다.
(캡처링은 IE8 이하에서 지원되지 않는다.)
addEventListener 메소드의 세번째 매개변수에 true를 설정하면 캡처링으로 전파되는 이벤트를 캐치하고 false 또는 미설정하면 버블링으로 전파되는 이벤트를 캐치한다.(`🚨이벤트리스너가 전파되는 이벤트를 캐치한다 뿐이지 실제로 연쇄적 반응인 캡처링과 버블링이 딱 하나만 실행되는 것은 아니다. 이 연쇄반응을 막는 방법을 맨 밑에서 다룬다.)
<!DOCTYPE html>
<html>
<head>
<style>
html { border:1px solid red; padding:30px; text-align: center; }
body { border:1px solid green; padding:30px; }
.top {
width: 300px; height: 300px;
background-color: red;
margin: auto;
}
.middle {
width: 200px; height: 200px;
background-color: blue;
position: relative; top: 34px; left: 50px;
}
.bottom {
width: 100px; height: 100px;
background-color: yellow;
position: relative; top: 34px; left: 50px;
line-height: 100px;
}
</style>
</head>
<body>
body
<div class="top">top
<div class="middle">middle
<div class="bottom">bottom</div>
</div>
</div>
<script>
// true: capturing / false: bubbling
const useCature = true;
const handler = function (e) {
const phases = ['capturing', 'target', 'bubbling'];
const node = this.nodeName + (this.className ? '.' + this.className : '');
// eventPhase: 이벤트 흐름 상에서 어느 phase에 있는지를 반환한다.
// 0 : 이벤트 없음 / 1 : 캡처링 단계 / 2 : 타깃 / 3 : 버블링 단계
console.log(node, phases[e.eventPhase - 1]);
alert(node + ' : ' + phases[e.eventPhase - 1]);
};
document.querySelector('html').addEventListener('click', handler, useCature);
document.querySelector('body').addEventListener('click', handler, useCature);
document.querySelector('div.top').addEventListener('click', handler, useCature);
document.querySelector('div.middle').addEventListener('click', handler, useCature);
document.querySelector('div.bottom').addEventListener('click', handler, useCature);
</script>
</body>
</html>
8. Event 객체
`event` 객체는 이벤트를 발생시킨 요소와 발생한 이벤트에 대한 유용한 정보를 제공한다.
이벤트가 발생하면 event 객체는 동적으로 생성되며 이벤트를 처리할 수 있는 이벤트 핸들러에 인자로 전달된다.
<!DOCTYPE html>
<html>
<body>
<p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
function showCoords(e) { // e: event object
const msg = document.querySelector('.message');
msg.innerHTML =
'clientX value: ' + e.clientX + '<br>' +
'clientY value: ' + e.clientY;
}
addEventListener('click', showCoords);
</script>
</body>
</html>
위와 같이 `event` 객체는 이벤트 핸들러에 암묵적으로 전달된다. 그러나 이벤트 핸들러를 선언할 때, event 객체를 전달받을 첫번째 매개변수를 명시적으로 선언하여야 한다. 예제에서 `e`라는 이름으로 매개변수를 지정하였으나 다른 매개변수 이름을 사용하여도 상관없다.
<!DOCTYPE html>
<html>
<body>
<em class="message"></em>
<script>
function showCoords(e, msg) {
msg.innerHTML =
'clientX value: ' + e.clientX + '<br>' +
'clientY value: ' + e.clientY;
}
const msg = document.querySelector('.message');
addEventListener('click', function (e) {
showCoords(e, msg);
});
</script>
</body>
</html>
8.1 Event.Property(🌟target, 🌟currentTarget, type, cancelable, eventPhase)
8.1.1 🌟 Event.target
실제로 이벤트를 발생시킨 요소를 가리킨다(event.currentTarget과는 차이가 있다.).
아래 예제를 살펴보자.
<!DOCTYPE html>
<html>
<body>
<div class="container">
<button id="btn1">Hide me 1</button>
<button id="btn2">Hide me 2</button>
</div>
<script>
function hide(e) {
e.target.style.visibility = 'hidden';
// 동일하게 동작한다.
// this.style.visibility = 'hidden';
}
document.getElementById('btn1').addEventListener('click', hide);
document.getElementById('btn2').addEventListener('click', hide);
</script>
</body>
</html>
hide 함수를 특정 노드에 한정하여 사용하지 않고 범용적으로 사용하기 위해 event 객체의 target 프로퍼티를 사용하였다. 위 예제의 경우, hide 함수 내부의 e.target은 언제나 이벤트가 바인딩된 요소를 가리키는 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>
</head>
<body>
<div class="container">
<button id="btn1">Hide me 1</button>
<button id="btn2">Hide me 2</button>
</div>
<script>
const container = document.querySelector('.container');
/**
*
* @param {MouseEvent} e
*
*/
function hide(e) {
// e.target은 실제로 이벤트를 발생시킨 DOM 요소를 가리킨다.
e.target.style.visibility = 'hidden';
// this는 이벤트에 바인딩된 DOM 요소(.container)를 가리킨다. 따라서 .container 요소를 감춘다.
// this.style.visibility = 'hidden';
console.log(this);
// <div class='container'>
// <button id='btn1' style='visibility: hidden;'>
// Hide me 1
// </button>
// <button id='btn2'>Hide me 2</button>
// </div>;
console.log(e.currentTarget);
// <div class='container'>
// <button id='btn1' style='visibility: hidden;'>
// Hide me 1
// </button>
// <button id='btn2'>Hide me 2</button>
// </div>;
console.log(e.currentTarget.nodeName); // DIV
console.log(e.target); // <button id="btn1" style="visibility: hidden;">Hide me 1</button>
console.log(e.target.nodeName); // BUTTON
}
container.addEventListener('click', hide);
</script>
</body>
</html>
See the Pen Event.target by Olimjo (@OLIMJO) on CodePen.
위 예제의 경우, this는 이벤트에 바인딩된 DOM 요소(.container)를 가리킨다. 따라서 만약 this 혹은 currentTarget을 hide하도록 했으면 container 요소를 감춘다. 하지만, `e.target`은 실제로 이벤트를 발생시킨 DOM 요소(button 요소 또는 button 요소를 포함한 .container 요소)를 정확하게 가리킨다. 헷갈릴 수도 있어 위 코드에서 해당 콘솔 결과를 주석으로 표시했으니 보고 이해하도록 하자. 얘기하고 싶은 것은 `Event.target`은 `addEventListener` 메소드가 바인딩 되어있는 `this`와 일치할 수도 있지만 반드시 일치하지는 않는다는 뜻이다.
▶그런데 container의 eventListener가 어떻게 container 내부 버튼의 이벤트를 대신 캐치한걸까?
container 내부의 button 요소에서 발생한 `click` 이벤트가 버블링되어서 컨테이너에 이벤트가 위임되어 이벤트 핸들러가 작동한 것이다.(이는 이벤트 위임 부분에서 자세하게 다룬다.)
8.1.2 🌟 Event.currentTarget
이벤트에 바인딩된 DOM 요소를 가리킨다. 즉, addEventListener 앞에 기술된 객체를 가리킨다. event.target과는 다르다.
addEventListener 메소드에서 지정한 이벤트 핸들러 내부의 this는 이벤트에 바인딩된 DOM 요소를 가리키며 이것은 이벤트 객체의 currentTarget 프로퍼티와 같다. 따라서 이벤트 핸들러 함수 내에서 currentTarget과 this는 언제나 일치한다.
<!DOCTYPE html>
<html>
<head>
<style>
html, body { height: 100%; }
div { height: 100%; }
</style>
</head>
<body>
<div>
<button>배경색 변경</button>
</div>
<script>
function bluify(e) {
// this: 이벤트에 바인딩된 DOM 요소(div 요소)
console.log('this: ', this);
// target: 실제로 이벤트를 발생시킨 요소(button 요소 또는 div 요소)
console.log('e.target:', e.target);
// currentTarget: 이벤트에 바인딩된 DOM 요소(div 요소)
console.log('e.currentTarget: ', e.currentTarget);
// 언제나 true
console.log(this === e.currentTarget);
// currentTarget과 target이 같은 객체일 때 true
console.log(this === e.target);
// click 이벤트가 발생하면 이벤트를 발생시킨 요소(target)과는 상관없이 this(이벤트에 바인딩된 div 요소)의 배경색이 변경된다.
this.style.backgroundColor = '#A5D9F3';
}
// div 요소에 이벤트 핸들러가 바인딩되어 있다.
// 자식 요소인 button이 발생시킨 이벤트가 버블링되어 div 요소에도 전파된다.
// 따라서 div 요소에 이벤트 핸들러가 바인딩되어 있으면 자식 요소인 button이 발생시킨 이벤트를 div 요소에서도 핸들링할 수 있다.
document.querySelector('div').addEventListener('click', bluify);
</script>
</body>
</html>
8.1.3 Event.type
발생한 이벤트의 종류를 나타내는 문자열을 반환한다.
<!DOCTYPE html>
<html>
<body>
<p>키를 입력하세요</p>
<em class="message"></em>
<script>
const body = document.querySelector('body');
function getEventType(e) {
console.log(e);
document.querySelector('.message').innerHTML = `${e.type} : ${e.keyCode}`;
}
body.addEventListener('keydown', getEventType);
body.addEventListener('keyup', getEventType);
</script>
</body>
</html>
예를 들어 `e.type`이 `keydown` 혹은 `keyup`을 가리킬 수 있다.
8.1.4 Event.cancelable
요소의 기본 동작을 취소시킬 수 있는지 여부(true/false)를 나타낸다.(취소가 불가능한 이벤트들도 있다는 뜻)
<!DOCTYPE html>
<html>
<body>
<a href="poiemaweb.com">Go to poiemaweb.com</a>
<script>
const elem = document.querySelector('a');
elem.addEventListener('click', function (e) {
console.log(e.cancelable);
// 기본 동작을 중단시킨다.
e.preventDefault();
});
</script>
</body>
</html>
8.1.5 Event.eventPhase
이벤트 (전파)흐름(event flow) 상에서 어느 단계(event phase)에 있는지를 반환한다.
| 반환값 | 의미 |
| 0 | 이벤트 없음 |
| 1 | 캡처링 단계 |
| 2 | 타깃 |
| 3 | 버블링 단계 |
9. 🌟Event Delegation (이벤트 위임)
우선 아래 예제를 살펴보자.
<ul id="post-list">
<li id="post-1">Item 1</li>
<li id="post-2">Item 2</li>
<li id="post-3">Item 3</li>
<li id="post-4">Item 4</li>
<li id="post-5">Item 5</li>
<li id="post-6">Item 6</li>
</ul>
모든 li 요소가 클릭 이벤트에 반응하는 처리를 구현하고 싶은 경우, li 요소에 이벤트 핸들러를 바인딩하면 총 6개의 이벤트 핸들러를 바인딩하여야 한다.
function printId() {
console.log(this.id);
}
document.querySelector('#post-1').addEventListener('click', printId);
document.querySelector('#post-2').addEventListener('click', printId);
document.querySelector('#post-3').addEventListener('click', printId);
document.querySelector('#post-4').addEventListener('click', printId);
document.querySelector('#post-5').addEventListener('click', printId);
document.querySelector('#post-6').addEventListener('click', printId);
만일 li 요소가 100개라면 100개의 이벤트 핸들러를 바인딩하여야 한다. 이는 실행 속도 저하의 원인이 될 뿐 아니라 코드 또한 매우 길어지며 작성 또한 불편하다.
그리고 동적으로 li 요소가 추가되는 경우, 아직 추가되지 않은 요소는 DOM에 존재하지 않으므로 이벤트 핸들러를 바인딩할 수 없다. 이러한 경우 이벤트 위임을 사용한다.
이벤트 위임(Event Delegation)은 다수의 자식 요소에 각각 이벤트 핸들러를 바인딩하는 대신 하나의 부모 요소에 이벤트 핸들러를 바인딩하는 방법이다. 위의 경우 6개의 자식 요소에 각각 이벤트 핸들러를 바인딩하는 것 대신 부모 요소(ul#post-list)에 이벤트 핸들러를 바인딩하는 것이다.
또한 DOM 트리에 새로운 li 요소를 추가하더라도 이벤트 처리는 부모 요소인 ul 요소에 위임되었기 때문에 새로운 요소에 이벤트를 핸들러를 다시 바인딩할 필요가 없다.
이는 이벤트가 이벤트 흐름에 의해 이벤트를 발생시킨 요소의 부모 요소에도 영향(버블링)을 미치기 때문에 가능한 것이다.
실제로 이벤트를 발생시킨 요소를 알아내기 위해서는 `Event.target`을 사용한다.
<!DOCTYPE html>
<html>
<body>
<ul class="post-list">
<li id="post-1">Item 1</li>
<li id="post-2">Item 2</li>
<li id="post-3">Item 3</li>
<li id="post-4">Item 4</li>
<li id="post-5">Item 5</li>
<li id="post-6">Item 6</li>
</ul>
<div class="msg">
<script>
const msg = document.querySelector('.msg');
const list = document.querySelector('.post-list')
list.addEventListener('click', function (e) {
// 이벤트를 발생시킨 요소
console.log('[target]: ' + e.target);
// 이벤트를 발생시킨 요소의 nodeName
console.log('[target.nodeName]: ' + e.target.nodeName);
// li 요소 이외의 요소에서 발생한 이벤트는 대응하지 않는다.
if (e.target && e.target.nodeName === 'LI') {
msg.innerHTML = 'li#' + e.target.id + ' was clicked!';
}
});
</script>
</body>
</html>
10. 기본 동작의 변경(기본 동작 막기, 이벤트 전파 막기)
이벤트 객체는 요소의 기본 동작과 요소의 부모 요소들이 이벤트에 대응하는 방법을 변경하기 위한 메소드는 가지고 있다.
10.1 Event.preventDefault()
폼을 submit하거나 링크를 클릭하면 다른 페이지로 이동하게 된다. 이와 같이 요소가 가지고 있는 기본 동작을 중단시키기 위한 메소드가 `preventDefault()`이다. 브라우저 기본 동작을 취소할 수 있는 방법은 두 가지가 있다.
- `event` 객체 사용하기. `event` 객체에 구현된 `event.preventDefault()` 메서드를 사용.
- 핸들러가 `addEventListener`가 아닌 `on<event>`를 사용해 할당되었다면 `false`를 반환하게 해 기본 동작을 막을 수도 있다.
아래 HTML에선 링크를 클릭해도 해당 URL로 이동하지 않는다.
<a href="/" onclick="return false">이곳</a>
이나
<a href="/" onclick="event.preventDefault()">이곳을</a> 클릭해주세요.
아래 예시에서는 `input`태그의 `checkbox`가 체크되어 있으면 `return false` 코드로 인해서 `preventDefault()`효과가 발생한다. 그래서 페이지 이동이 발생하지 않는다.(return false가 아니라 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>
<p>
<label>prevent event on</label>
<input id="prevent" type="checkbox" name="eventprevent" value="on" />
</p>
<p>
<a href="http://opentutorials.org">opentutorials</a>
</p>
<p>
<form action="http://opentutorials.org">
<input type="submit" />
</form>
</p>
<script>
document.querySelector('a').onclick = function(event){
if(document.getElementById('prevent').checked)
return false;
};
document.querySelector('form').onclick = function(event){
if(document.getElementById('prevent').checked)
return false;
};
</script>
</body>
</html>
- 핸들러에서 `false`를 반환하는 것은 예외 상황입니다.
원래 이벤트 핸들러에서 반환된 값은 대개 무시됩니다.
하나의 예외사항이 있는데 바로 `on<event>`를 사용해 할당한 핸들러에서 `false`를 반환하는 것입니다.
이 외의 값들은 `return` 되어도 무시됩니다. `true` 역시 무시되죠.
<!DOCTYPE html>
<html>
<body>
<a href="http://www.google.com">go</a>
<script>
document.querySelector('a').addEventListener('click', function (e) {
console.log(e.target, e.target.nodeName);
// a 요소의 기본 동작을 중단한다.
e.preventDefault();
});
</script>
</body>
</html>
10.1.1 addEventListener의 'passive' 옵션
`addEventListener`의 `passive: true` 옵션은 브라우저에게 `preventDefault()`를 호출하지 않겠다고 알리는 역할을 합니다.
이 옵션은 왜 필요한 걸까요?
모바일 기기에는 사용자가 스크린에 손가락을 대고 움직일 때 발생하는 `touchmove`와 같은 이벤트가 있습니다. 이런 이벤트는 기본적으로 스크롤링(scrolling)을 발생시킵니다. 그런데 핸들러의 `preventDefault()`를 사용하면 스크롤링을 막을 수 있습니다.
브라우저는 스크롤링을 발생시키는 이벤트를 감지했을 때 먼저 모든 핸들러를 처리하는데, 이때 `preventDefault`가 어디에서도 호출되지 않았다고 판단되면, 그제야 스크롤링을 진행합니다. 이 과정에서 불필요한 지연이 생기고, 화면이 ‘덜덜 떨리는’ 현상이 발생합니다.
`passive: true` 옵션은 핸들러가 스크롤링을 취소하지 않을 것이라는 정보를 브라우저에게 알려주는 역할을 합니다. 이 정보를 바탕으로 브라우저는 화면을 최대한 자연스럽게 스크롤링 할 수 있게 하고 이벤트는 적절하게 처리됩니다.
Firefox, Chrome 같은 몇몇 브라우저에서 touchstart 와 touchmove 이벤트의 passive 는 기본값이 true입니다.(설정할 필요가 없다는 뜻)
10.1.2 event.defaultPrevented
기본 동작을 막은 경우는 `event.defaultPrevented` 값이 `true` 이고, 그렇지 않은 경우는 `false` 입니다.
이를 이용한 흥미로운 유스 케이스가 있습니다.
버블링과 캡처링 챕터에서 배운 `event.stopPropagation()`를 기억하시나요? 여기서 버블링을 막는 게 왜 나쁜지 이야기한 바 있습니다.
가끔은 `event.stopPropagation()` 대신에 `event.defaultPrevented`를 사용해 이벤트가 적절히 처리되었다고 다른 이벤트에게 알릴 수도 있습니다.
실제 예시를 통해 이 말을 이해해봅시다.
브라우저에서 마우스 오른쪽 버튼을 클릭하면 `contextmenu`라는 이벤트가 발생합니다. 이 이벤트가 발생하면 컨텍스트 메뉴가 뜨죠. 그런데 컨텍스트 메뉴 대신 다른 걸 띄울 수도 있습니다. 아래와 같이 말이죠.
<button>마우스 오른쪽 버튼을 클릭하면 컨텍스트 메뉴가 뜹니다.</button>
<button oncontextmenu="alert('커스텀 메뉴가 뜨네요!'); return false">
여기서 마우스 오른쪽 버튼을 클릭해보세요.
</button>
이렇게 버튼에서만 자체 컨텍스트 메뉴를 띄우는 대신, 문서 레벨에서도 자체 컨텍스트 메뉴를 뜨게 할 수 있습니다.
마우스 오른쪽 버튼을 클릭하면 가장 가까운 컨텍스트 메뉴가 나타납니다.
<p>문서 레벨 컨텍스트 메뉴</p>
<button id="elem">버튼 레벨 컨텍스트 메뉴</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("버튼 컨텍스트 메뉴");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("문서 컨텍스트 메뉴");
};
</script>
그런데 위와 같이 구현하면 `elem`을 클릭했을 때 두 개의 컨텍스트 메뉴가 뜨는 문제가 발생합니다. 이벤트가 버블링되면서 버튼 레벨의 컨텍스트 메뉴와 문서 레벨의 컨텍스트 메뉴가 뜨는 것이죠.
어떻게 이 문제를 고칠 수 있을까요? 가장 먼저 떠오르는 생각은 "버튼에 구현된 마우스 우클릭 이벤트를 처리하고 나면 버블링이 멈추도록 하자"일 겁니다. 이때 `event.stopPropagation()`을 사용하겠죠.
<p>문서 레벨 컨텍스트 메뉴</p>
<button id="elem">버튼 레벨 컨텍스트 메뉴(event.stopPropagation를 사용해 버그 수정)</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
alert("버튼 컨텍스트 메뉴");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("문서 컨텍스트 메뉴");
};
</script>
이제 의도한 대로 버튼에서 마우스 오른쪽 버튼을 클릭하면 버튼 레벨의 컨텍스트 메뉴만 뜹니다. 하지만 이에 대한 대가가 너무 큽니다. 외부 코드를 사용해 더는 마우스 우클릭에 대한 정보를 얻을 수 없기 때문입니다. 통계 자료 수집을 위한 코드가 동작하지 못하죠. 현명하지 못한 해결책입니다.
`event.stopPropagation()`를 사용하는 것 대신에 `document` 핸들러에서 기본 동작이 막혀있는지 확인하면 문제를 해결할 수 있습니다. 기본 동작이 막혀있는데 이벤트를 핸들링하려는 경우, 이에 반응하지 않도록 하면 되죠.
<p>문서 레벨 컨텍스트 메뉴(event.defaultPrevented를 확인함)</p>
<button id="elem">버튼 레벨 컨텍스트 메뉴</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("버튼 컨텍스트 메뉴");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return; // 현재 캐치한 event가 defaultPrevented인가? 라고 묻는 것
event.preventDefault();
alert("문서 컨텍스트 메뉴");
};
</script>
이제 모든 기능이 의도한 대로 동작합니다. 중첩 요소가 몇 개 있고, 요소마다 각각의 컨텍스트 메뉴가 있는 경우도 이젠 의도한 대로 동작할 겁니다. 각 `contextmenu` 핸들러에서 `event.defaultPrevented`를 확인하면 되죠.
▶무분별한 `event.stopPropagation` 대신 `if (event.defaultPrevented) return;`와 같은 명령형 코드로 현재 이벤트 핸들러에서 캐치한 이벤트가 기본 동작이 방지된 이벤트인지 확인하여 조건에 따라 동작을 컨트롤한다.
10.2 Event.stopPropagation()
어느 한 요소를 이용하여 이벤트를 처리한 후 이벤트가 부모 요소로 이벤트가 전파되는 것을 중단시키기 위한 메소드이다. 부모 요소에 동일한 이벤트에 대한 다른 핸들러가 지정되어 있을 경우 사용된다.
아래 코드를 보면, 부모 요소와 자식 요소에 모두 `mousedown` 이벤트에 대한 핸들러가 지정되어 있다. 하지만 부모 요소와 자식 요소의 이벤트를 각각 별도로 처리하기 위해 button 요소의 이벤트의 전파(버블링)를 중단시키기 위해서는 `stopPropagation` 메소드를 사용하여 이벤트 전파를 중단할 필요가 있다.
<!DOCTYPE html>
<html>
<head>
<style>
html, body { height: 100%;}
</style>
</head>
<body>
<p>버튼을 클릭하면 이벤트 전파를 중단한다. <button>버튼</button></p>
<script>
const body = document.querySelector('body');
const para = document.querySelector('p');
const button = document.querySelector('button');
// 버블링
body.addEventListener('click', function () {
console.log('Handler for body.');
});
// 버블링
para.addEventListener('click', function () {
console.log('Handler for paragraph.');
});
// 버블링
button.addEventListener('click', function (event) {
console.log('Handler for button.');
// 이벤트 전파를 중단한다.
event.stopPropagation();
});
</script>
</body>
</html>
10.3 preventDefault & stopPropagation
`event.stopPropagation()`과 `return false`로 알려진 `event.preventDefault()`는 명백히 다른 메서드다. 두 메서드는 연관성이 없다. `preventDefault()`메소드를 사용했다고 `stopPropagation`의 역할까지 하지는 못한다.
기본 동작의 중단과 버블링 또는 캡처링의 중단을 동시에 실시하는 방법은 아래와 같다.
element.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// ...
});
이벤트 흐름의 어떤 단계에서든 preventDefault()를 호출하면 이벤트를 취소하기 때문에 기본동작을 막고싶다면 꼭 첫줄에 넣어야하며, 취소 불가능한 이벤트가 있기 때문에 `event.cancelable`을 통해 확인해보는 것도 좋을 것 같다
레퍼런스
- https://poiemaweb.com/js-event#1-introduction
- https://ko.javascript.info/default-browser-action
- https://www.designcise.com/web/tutorial/does-the-javascript-preventdefault-method-stop-event-propagation