let btn = document.querySelector('#myBtn');
btn.onclick = function () {
console.log('Hi Codeit!');
};
const myContent = document.querySelector('#content');
myContent.innerHTML = '<div>Hello</div>';
myContent.className = 'buttons';
onclick 프로퍼티를 사용하는 것도 한 가지 문제점이 있다. innerHTML이나 className과 같은 프로퍼티를 사용할 때도 프로퍼티에 어떤 값을 할당하는 방식은 기존에 있던 값을 덮어쓰는 방식으로 동작하기 때문에 기존에 있는 값을 유지하면서 일부만 수정하기에는 어렵다는 단점이 있었다.
onclick 프로퍼티도 새로운 이벤트를 할당하게 되면 기존의 값을 덮어쓰기 때문에 동작할 때 새로운 값이 나와버린다. 이렇게 하면 중요한 이벤트를 덮어쓰는 실수를 할 수도 있고, 또 여러 개의 이벤트 핸들러를 다룰 수 없다는 단점이 있다.
이러한 문제를 해결하기 위해서
let btn = document.querySelector('#myBtn');
function event1() {
console.log('Hi Codeit');
}
function event2() {
console.log('Hi again');
}
btn.onclick = function () {
event1();
event2();
};
이와 같이 하나의 이벤트 핸들러 안에 여러 개의 이벤트 핸들러를 넣는 방법이 있지만, 이마저도 중간에 새로운 이벤트를 추가하거나 혹은 이 중 하나를 제거해야 하는 상황은 대처하기 어려운 문제도 있다. 각 이벤트별로 중요한 리턴값이 있을 경우 그 리턴값도 다루어야 하는 훨씬 더 복잡한 문제가 생긴다. 그래서 onclick 프로퍼티를 이용하지 않고 이벤트 핸들러를 이용하는 방법이 또 하나 있다. 바로 addEventListener를 활용하는 것이다.
let btn = document.querySelector('#myBtn');
function event1() {
console.log('Hi codeit');
}
function event2() {
console.log('Hi again');
}
// elem.addEventListener(event, handler)
btn.addEventListener('click', event1);
btn.addEventListener('click', event2);
이 방법이 이벤트 핸들러를 등록할 때 가장 권장되는 방법이다. 이벤트 핸들러를 등록하고 싶은 요소에 addEventListener를 호출하는데, 첫 번째 파라미터로 이벤트 타입을 문자열로 전달하고 두 번째 파라미터로 이벤트 핸들러를 전달한다. 이렇게 메소드를 여러번 호출하게 되면 하나의 요소에 여러 개의 독립적인 이벤트 핸들러를 적용할 수 있다. 이렇게 등록한 이벤트는 removeEventListener 메소드로 이벤트 핸들러를 개별적으로 제거할 수도 있다.
btn.removeEventListener('click', event2);
등록할 때와 같이 첫 번째 파라미터로 이벤트 타입을 전달하고 두 번째 파라미터로 이벤트 핸들러를 전달하면 해당 이벤트 핸들러가 삭제되는 방식이다. 주의해야 할 점은 이벤트를 등록할 때 사용했던 이벤트 핸들러 그대로를 전달해야 한다는 점이다. 이벤트를 등록할 때 핸들러 부분에서 함수를 바로 작성할 수도 있기 때문에 등록할 때 사용했던 함수를 삭제할 때도 그대로 써주면 잘 작동할 것 같지만 그렇지 않다. 그러나 모양은 똑같지만 서로 다른 함수이기 때문에 이벤트 핸들러가 정상적으로 삭제되지 않는다.
// 정상적으로 삭제되지 않음
btn.addEventListener('click', function() {
console.log('event3');
});
btn.removeEventListener('click', function() {
console.log('event3');
});
그렇기 때문에 이벤트 핸들러를 삭제해야 할 수도 있다면 이벤트를 등록할 때 외부에 함수를 만들어서 해당 함수의 이름으로 핸들러를 전달해주어야 한다는 점을 꼭 기억해야 한다.
또한 이벤트 핸들러 부분에는 함수 이름만 전달해주면 된다. 간혹 함수명 뒤에 소괄호를 붙여주는 실수를 하는 경우가 있다. 이렇게 하면 함수를 해석할 때 함수를 그대로 해석해버려서 함수의 리턴값으로 undefined가 두 번째 파라미터로 전달되어 버려서 값이 undefined가 출력되어 사실상 이벤트 핸들러가 등록되지 않는다.
| 이벤트 타입 | 설명 |
|---|---|
| mousedown | 마우스 버튼을 누르는 순간 |
| mouseup | 마우스 버튼을 눌렀다 떼는 순간 |
| click | 왼쪽 버튼을 클릭한 순간 |
| dbclick | 왼쪽 버튼을 빠르게 두 번 클릭한 순간 |
| contextmenu | 오른쪽 버튼을 클릭한 순간 |
| mousemove | 마우스를 움직이는 순간 |
| mouseover | 마우스 포인터가 요소 위로 올라온 순간 |
| mouseout | 마우스 포인터가 요소에서 벗어나는 순간 |
| mouseenter | 마우스 포인터가 요소 위로 올라온 순간 (버블링이 일어나지 않음) |
| mouseleave | 마우스 포인터가 요소에서 벗어나는 순간 (버블링이 일어나지 않음) |
| 이벤트 타입 | 설명 |
|---|---|
| keydown | 키보드의 버튼을 누르는 순간 |
| keypress | 키보드의 버튼을 누르는 순간('a', '5'등 출력이 가능한 키에서만 동작하며, shift, Esc드의 키에는 반응하지 않음) |
| keyup | 키보드의 버튼을 눌렀다 떼는 순간 |
| 이벤트 타입 | 설명 |
|---|---|
| focusin | 요소에 포커스가 되는 순간 |
| focusout | 요소로부터 포커스가 빠져나가는 순간 |
| focus | 요소에 포커스가 되는 순간 (버블링이 일어나지 않음) |
| blur | 요소로부터 포커스가 빠져나가는 순간 (버블링이 일어나지 않음) |
| 이벤트 타입 | 설명 |
|---|---|
| change | 입력된 값이 바뀌는 순간 |
| input | 값이 입력되는 순간 |
| select | 입력 양식의 하나가 선택되는 순간 |
| submit | 폼을 전송하는 순간 |
| 이벤트 타입 | 설명 |
|---|---|
| scroll | 스크롤 바가 움직일 때 |
| 이벤트 타입 | 설명 |
|---|---|
| resize | ㅣ윈도우 사이즈를 움직일 때 발생 |
이벤트를 제대로 다루기 위해서는 이벤트에 대한 상세한 정보가 필요하다. 정확히 언제 클릭했는지, 클릭하는 순간에 마우스 포인터의 위치는 어디에 있는지, 키보드 이벤트가 발생했다면 어떤 키를 눌렀는지 등에 대한 정보가 필요하다.
웹페이지에서 어떤 이벤트가 발생하면 그 이벤트와 관련된 다양한 정보를 담고 있는 이벤트 객체가 자동으로 만들어진다. 이벤트 핸들러의 첫번째 파라미터에는 항상 이벤트 객체가 전달된다. 이벤트 객체에는 굉장히 많은 프로퍼티를 가지고 있는데, 그 중 공통적으로 많이 사용되는 프로퍼티는 type과 target 프로퍼티다.
✏️type property: 발생한 이벤트의 타입
✏️target property: 이벤트가 발생한 해당 요소. 특히나 target은 DOM 요소를 담고 있기 때문에 이벤트가 발생했을 때 해당요소를 수정한다거나 그 요소가 가지고 있는 속성값들을 참조해야 하는 상황에서 유용하게 사용할 수 있다.
const myInput = document.querySelector('#myInput');
const myBtn = document.querySelector('#myBtn');
function printEvent(event) {
console.log(event);
}
myInput.addEventListener('keydown', printEvent);
myBtn.addEventListener('click', printEvent);
const myInput = document.querySelector('#myInput');
const myBtn = document.querySelector('#myBtn');
myInput.addEventListener('keydown', function() {
console.log('keyboardEvent');
});
const myInput = document.querySelector('#myInput');
const myBtn = document.querySelector('#myBtn');
myBtn.addEventListener('click', function() {
console.log('MouseEvent');
});
| 프로퍼티 | 설명 |
|---|---|
| type | 이벤트 이름('click', 'mouseup','keydown'등) |
| target | 이벤트가 발생한 요소 |
| currentTarget | 이벤트 핸들러가 등록된 요소 |
| timeStamp | 이벤트 발생시각(페이지가 로드된 이후부터 경과한 밀리초) |
| bubbles | 버블링 단계인지를 판단하는 값 |
| 프로퍼티 | 설명 |
|---|---|
| button | 누른 마우스의 버튼 (0: 왼쪽, 1: 가운데(휠), 2:오른쪽) |
| clientX, clientY | 마우스 커서의 브라우저 표시 영역에서의 위치 |
| pageX, pageY | 마우스 커서의 문서 영역에서의 위치 |
| offsetX, offsetY | 마우스 커서의 이벤트 발생한 요소에서의 위치 |
| screenX, screenY | 마우스 커서의 모니터 화면 영역에서의 위치 |
| altKey | 이벤트가 발생할 때 alt키를 눌렀는지 |
| ctrlKey | 이벤트가 발생할 때 ctrl키를 눌렀는지 |
| shiftKey | 이벤트가 발생할 때 shift키를 눌렀는지 |
| metaKey | 이벤트가 발생할 때 meta키를 눌렀는지 (window는 window키, mac은 cmd키) |
| 프로퍼티 | 설명 |
|---|---|
| key | 누른 키가 가지고 있는 값 |
| code | 누른 키의 물리적인 위치 |
| altKey | 이벤트가 발생할 때 alt키를 눌렀는지 |
| ctrlKey | 이벤트가 발생할 때 ctrl키를 눌렀는지 |
| shiftKey | 이벤트가 발생할 때 shift키를 눌렀는지 |
| metaKey | 이벤트가 발생할 때 meta키를 눌렀는지 (window는 window키, mac은 cmd키) |
const content = document.querySelector('#content');
const title = document.querySelector('#title');
const list = document.querySelector('#list');
const items = document.querySelectorAll('.item');
content.addEventListener('click', function() {
console.log('content Event');
});
title.addEventListener('click', function() {
console.log('title Event');
});
list.addEventListener('click', function() {
console.log('list Event');
});
for(let item of items) {
item.addEventListener('click', function() {
console.log('item Event');
});
}
버블링의 원리는 하나의 요소에 이벤트가 발생하게 되면 이 요소에 할당된 이벤트가 동작하고, 거기서 끝이 아니라 이어서 같은 타입의 이벤트에 한해서 부모 요소의 핸들러도 동작하는 것이다.
그렇게 가장 최상단의 윈도우 객체를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 모든 이벤트 핸들러가 동작하는 원리이다. 이벤트가 발생하면 가장 아래에 자식요소부터 부모요소를 거슬러 올라가며 발생하는 모양이 마치 물속에서 올라오는 거품과 닮았다고 해서 버블링이라는 이름이 붙었다고 한다.
이러한 이유로, 아이템 영역을 클릭했지만 버블링에 의해 아이템의 부모요소인 리스트, 리스트의 부모요소인 컨텐트도 함께 순서대로 동작한 것이다.
여기서 한 가지 주의해야 할 점이 있는데, 지난 시간에 배운 이벤트 객체의 target 프로퍼티이다.
const content = document.querySelector('#content');
const title = document.querySelector('#title');
const list = document.querySelector('#list');
const items = document.querySelectorAll('.item');
content.addEventListener('click', function(e) {
console.log('content Event');
console.log(e.target);
});
title.addEventListener('click', function(e) {
console.log('title Event');
console.log(e.target);
});
list.addEventListener('click', function(e) {
console.log('list Event');
console.log(e.target);
});
for (let item of items) {
item.addEventListener('click', function(e) {
console.log('item Event');
console.log(e.target);
});
}
부모요소의 핸들러들이 최초에 이벤트가 발생한 위치를 정확하게 파악할 수 있게 되는 것인데 이벤트 버블링이 일어나도 이벤트 객체의 타켓 프로퍼티는 변하지 않고 처음 이벤트가 발생한 시작점을 담고 있다.
그런데 만약 실행 중인 핸들러가 할당된 요소, 이벤트 핸들러가 등록된 요소에 접근하고 싶은 경우 currentTarget 프로퍼티를 활용하면 된다. 실제로 이벤트 핸들러가 동작되는 요소가 출력된다.
const content = document.querySelector('#content');
const title = document.querySelector('#title');
const list = document.querySelector('#list');
const items = document.querySelectorAll('.item');
content.addEventListener('click', function(e) {
console.log('content Event');
console.log(e.currentTarget);
});
title.addEventListener('click', function(e) {
console.log('title Event');
console.log(e.currentTarget);
});
list.addEventListener('click', function(e) {
console.log('list Event');
console.log(e.currentTarget);
});
for (let item of items) {
item.addEventListener('click', function(e) {
console.log('item Event');
console.log(e.currentTarget);
});
}
버블링을 멈추는 방법은 이벤트 객체에 stopPropagation이라는 메소드를 활용하면 간단하게 버블링을 멈출 수 있다. 그러나 가급적 버블링을 막는 일은 피하는 것이 좋다. 이벤트 버블링을 막아버리면 바로 위에 있는 모든 부모 요소의 입장에서 상위 요소 영역만큼의 이벤트를 발생시킬 범위가 사라져버린다.
for (let item of items) {
item.addEventListener('click', function(e) {
console.log('item Event');
console.log(e.currentTarget);
e.stopPropagation();
});
}
이벤트에는 버블링 이외에도 '캡처링(capturing)'이라는 흐름이 존재한다. 실제 코드에서 자주 쓰이지는 않지만, 상황에 따라 필요할 수도 있다.
먼저, 표준 DOM 이벤트에서 정의한 이벤트 흐름에는 3가지 단계가 있다.
1. 캡처링 단계: 이벤트가 하위 요소로 전파되는 단계
2. 타겟 단계: 이벤트가 실제 타켓 요소에 전달되는 단계
3. 버블링 단계: 이벤트가 상위 요소로 전파되는 단계
타겟 단계는 이벤트 객체의 target 프로퍼티가 되는 요소에 등록되어있던 이벤트 핸들러가 동작하는 단계인데, 쉽게 생각해서 가장 처음 이벤트 핸들러가 동작하게 되는 순간이라고 생각하면 된다.
버블링은 내가 선택한 태그로부터 한 단계씩 위로 올라가며 이벤트가 전파되는 것이라면, 캡처링은 이벤트가 발생하면 가장 먼저, 그리고 버블링의 반대 방향으로 진행되는 이벤트 전파 방식이다.

이벤트가 발생하면 가장 먼저 window 객체에서부터 target 까지 이벤트 전파가 일어난다. (캡처링 단계) 그리고 나서 타겟에 도달하면 타겟에 등록된 이벤트 핸들러가 동작하고, (타켓 단계) 이후 다시 window 객체로 이벤트가 전파된다. (버블링 단계)
이런 과정을 통해 각 요소에 할당된 이벤트 핸들러가 호출되는데, 캡처링 단계에서 이벤트를 발생시켜야 하는 일은 매우 드문 경우이다. 보통 타겟 단계에서 target에 등록된 이벤트 핸들러가 있으면 해당 이벤트 핸들러가 먼저 동작한이 후에 버블링 단계에서 각 부모 요소에 등록된 이벤트 핸들러가 있으면 그 때 해당 이벤트 핸들러가 동작하는 것이 일반적이다.
하지만 상황에 따라서는 캡처링 단계에서 부모 요소의 이벤트 핸들러를 동작시켜야 할 수도 있다. 캡처링 단계에서 이벤트 핸들러를 동작시키려면, addEventListener에 세번째 프로퍼티에 true 또는 {capture:true} 를 전달하면 된다.
자식 요소에서 발생한 이벤트를 부모 요소에서 다루는 방법을 말한다. 자식 요소의 이벤트를 부모 요소에서 위임한 것이라고 볼 수 있다.
const list = document.querySelector('#list');
for (let item of list.children) {
item.addEventListener('click', function(e) {
e.target.classList.toggle('done');
});
}
const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);
처음에는 없다가 새로운 아이템을 추가하는 상황이 되었을 때, 추가된 아이템에는 이벤트 핸들러가 적용되지 않는다. 이럴 때 이벤트 버블링을 활용하면 간단하게 해결할 수 있다.
버블링 개념에 따르면 부모요소가 자식요소에서 발생한 이벤트를 감지할 수 있고, 이벤트 객체에 타겟 프로퍼티가 항상 이벤트 발생위치를 담고 있기 때문에 아이템 각각에 이벤트 핸들러를 등록하는 것이 아니라 부모요소에 이벤트 하나만 등록해주어도 모든 자식 요소를 다룰 수 있다.
const list = document.querySelector('#list');
list.addEventListener('click', function(e) {
e.target.classList.toggle('done');
});
const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);
그러나 이렇게 코드를 수정해도 한 가지 문제가 남아있다. 부모 요소에게 등록된 이벤트 핸들러이기 때문에 자식 요소를 제외한 온전히 부모인 요소를 클릭해도 이벤트 핸들러가 동작한다는 것이다.
때문에 이벤트 위임을 할 때는 명확히 원하는 요소에서 의도한 동작이 일어나게끔 따로 처리를 해야한다.
const list = document.querySelector('#list');
list.addEventListener('click', function(e) {
//if (e.target.tagName === 'LI')
if (e.target.classList.contains('item')) {
e.target.classList.toggle('done');
});
const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);
주석에 써둔 것처럼 target 프로퍼티의 네임이 li인지 확인하거나, classList의 contains 메소드를 활용해서 item이라는 class 메소드를 가지고 있는지 확인하면 해결할 수 있다.
tagName 프로퍼티는 말 그대로 해당요소의 태그 이름 값을 대문자로 담고 있는지 확인하는 프로퍼티고, classList의 contains 메소드는 파라미터로 전달하는 값이 해당요소의 class속성에 있는지 확인해서 boolean 형태로 결과를 리턴해주는 메소드이다.
이벤트 위임을 활용하면 새로운 자식요소를 추가하거나 삭제하더라도 이벤트에 대한 제어를 자식요소에 신경쓰지 않아도 되기 때문에 훨씬 더 유연하게 코드를 작성할 수 있다는 장점이 있다.
뿐만 아니라 여러 개의 이벤트를 만들지 않아도 된다는 것은 코드를 적게 작성할 수 있으며, 실제로 이 코드가 동작할 때 프로그램의 성능에도 긍정적인 영향을 미친다.
코드를 작성할 때 이벤트 위임을 먼저 고려 후, 불가피한 경우에만 개별적인 이벤트 핸들러를 사용하는게 더 좋은 방법이라고 볼 수 있다.
const list = document.querySelector('#list');
list.addEventListener('click', function(e) {
//if (e.target.tagName === 'LI')
if (e.target.classList.contains('item')) {
e.target.classList.toggle('done');
});
const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);
li.addEventListener('click', function(e) {
e.stopPropagation();
});
참고로 이벤트 위임은 버블링을 활용한 방식이기 때문에, 당연히 자식요소 중에 버블링을 막는 이벤트가 있을 경우 버블링을 막기 때문에 우리가 의도한대로 이벤트가 작동하지 않는다. 그래서 불가피한 경우가 아니면 이벤트 버블링을 막는 일은 피하는 것이 좋다.
대부분의 이벤트는 발생하는 순간 각 태그별로 혹은 문서 전체적인 측면에서 브라우저가 기본적으로 가지고 있는 어떤 동작들을 수행한다. 상황에 따라서는 이러한 기본 동작들을 사용하고 싶어하지 않을 수 있다. 자바스크립트를 사용하면 기본 동작들을 막을 수 있다.
이벤트 객체에 preventDefault 메소드를 사용하는 것이다.
const link = document.querySelector('#link');
const checkbox = document.querySelector('#checkbox');
const input = document.querySelector('#input');
const text = document.querySelector('#text');
link.addEventListener('click', function(e) {
e.preventDefault();
alert('지금은 이동할 수 없습니다.');
});
input.addEventListener('keydown', function(e) {
if (!checkbox.checked) {
e.preventDefault();
alert('체크박스를 먼저 클릭해 주세요.');
}
});
text.addEventListener('contextmenu', function(e) {
e.preventDefault();
alert('마우스 오른쪽 클릭은 사용할 수 없습니다.');
});
그런데 이벤트 버블링을 막는 것을 조심해야 한다고 했던 것과 같이, 브라우저의 기본 동작을 막는 일도 꼭 필요한 일인지 생각해보아야 한다. 특히나 HTML 태그에 존재하는 브라우저의 기본 동작을 막는 경우에는 각 HTML 태그들이 가지고 있는 고유한 역할과 의미를 훼손시킬 수 있기 때문에 꼭 필요한 경우에만 사용할 수 있도록 해야한다.
정리가 잘 된 글이네요. 도움이 됐습니다.