TIL 21 | 이벤트 핸들러

43295 단어 JavaScriptJavaScript

1. onclick 프로퍼티

(1) DOM 요소에 접근한 뒤 onclick 프로퍼티 사용

let btn = document.querySelector('#myBtn');

btn.onclick = function () {
	console.log('Hi Codeit!');
};

(2) html 태그에 직접 onclick 속성을 활용하는 방법

<button onclick="console.log('Hello Codeit!')">HTML Click!</button>

onclick 프로퍼티의 문제점

새로운 이벤트 핸드러를 추가할 시 기존 이벤트가 무시되고 새로운 이벤트만 덮어씌워져 작동한다. 이로인해 의도하지 않게 중요한 이벤트가 작동하지 않을 수 있고 여러개 이벤트를 동시에 실행할 수 없다.

let btn = document.querySelector('#myBtn');

btn.onclick = function () {
	console.log('Hi Codeit!');
};

btn.onclick = function () {
	console.log('Hi agian');
}; // => 위에 Hi Codeit은 작동 안됨, 새로 추가된 이벤트로 인해 먼저 추가된 이벤트가 무시됨

이러한 문제를 해결하기 위해 여러개의 이벤트 핸들러를 배치하는 방식을 사용할 수 있다.

function event1() {
	console.log('Hi Codeit!');
}

function event2() {
	console.log('Hi again!');
}

btn.onclick = function () {
 event1();
 event2();
}; 

그러나 이마저도 새로운 이벤트를 추가하거나 제거할 때 대처하기가 어렵고 내부에 들어가는 이벤트 함수가 복잡해질수록 핸들링하기 어려워진다.

2. elem.addEventListner(event, handler)

addEventListner 를 사용하면 onclick 프로퍼티 이벤트의 단점을 보완할 수 있다. *가장 권장되는 방법

function event1() {
	console.log('Hi Codeit!');
}

function event2() {
	console.log('Hi again!');
}

btn.addEventListener('click', event1); 
btn.addEventListener('click', event2);

onclick 프로퍼티와는 달리 하나의 요소에 여러개의 이벤트 요소를 독립적으로 적용시킬 수 있다. 이벤트 핸들러 부분에는 함수 이름만 전달한다. event1()처럼 함수 호출식을 적으면 함수의 리턴값이 핸들러에 적용되어 의도한대로 작동하지 않는다.

// elem.removeEventListener(event, handler) 
btn.removeEventListener('click', event2);

removeEvenfListener 로 이벤트 핸들러를 사용하면 이벤트를 개별적으로 삭제할 수 있다. removeEventListener를 사용하여 이벤트를 삭제할때는 등록했던 핸들러의 이름을 사용해야한다.

btn.addEventListener('click', function(){
  console.log('event3')
});

btn.removeEventListener('click', function(){
  console.log('event3') // 원하는 이벤트가 삭제되지 않는다. 

가령 핸들러부분에 함수를 추가하면 함수의 모양이 같아 등록한 함수와 제거 함수가 동일하게 보일 수는 있지만 이는 다른 함수로 의도한대로 핸들러가 삭제되지 않는다. removeEventListener는 파라미터로 전달하는 타입과 이벤트 핸들러가, addEventListener 메소드로 등록할 때와 동일 할 때만 이벤트 핸들러를 삭제할 수 있다. addEventListener가 아닌 onclick 프로퍼티로 등록된 이벤트는 핸들러의 이름이 같아도 삭제할 수 없다.

3. 이벤트 객체 (Event Object)

웹페이지에서 이벤트가 발생하면 이벤트 정보를 담은 이벤트 객체가 자동으로 생성된다. 이벤트 객체 내 프로퍼티 중 주로 사용하는 프로퍼티는 타겟과 타입이 있다. type은 발생한 이벤트의 타입을 담고 있고, target은 이벤트가 발생한 요소를 담고 있다.

const myInput = document.querySelector('#myInput');
const myBtn = document.querySelector('#myBtn');

function printEvent(event) {
  console.log(event); // 이벤트 객체 확인
  event.target.style.color = 'red'; // 이벤트가 발생할 때마다 해당 돔 요소에 스타일을 적용한다.
  console.log(event.target) // 이벤트 타겟의 html 요소를 알 수 있는 코드 
}

myInput.addEventListener('keydown', printEvent);
myBtn.addEventListener('click', printEvent);

4. 버블링

한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 같은 이벤트 타입의 부모 요소의 핸들러가 동작한다. 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.

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)// 타겟을 보면 최초로 이벤트가 실행된 요소가 출력된다.
  // 이벤트 버블링이 일어나도 타겟 프로퍼티는 이벤트 시작 요소를 담고 있다. 
  console.log(e.currentTarget); 
  // curret Traget은 현재 동작하고 있는 이벤트의 요소를 확인할 수 있다. 
});

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의 이벤트를 동작시키면, 버블링에 의해 list, content영역의 이벤트가 연달아 동작한다. 
  item.addEventListener('click', function(e) {
    console.log('item Event');
  });
} 

버블링을 막기 위해선 stopPropagation()이라는 메소드를 사용하면 된다.

for (let item of items) { 
  item.addEventListener('click', function(e) {
    console.log('item Event');
    console.log(e.currentTarget);
    e.stopPropagation();
  });

stopPropagation()을 활용하면 해당 요소의 버블링을 막을 수 있다. 그러나 그 위 부모 요소의 버블링까지 막아주지는 않는다.

정말 필요한 경우가 아니라면 가급적 버블링을 막는일은 피하는 것이 좋다. 하위 요소의 버블링을 막아버리면 모든 부모요소에서 하위 요소의 이벤트를 동작시킬 수 있는 범위를 잃게 된다. 예를들어 페이지 전체에 걸친 이벤트를 동작하고 싶은 경우 document나 body와 같은 상위요소에 이벤트 핸들러를 만들어줄텐데 버블링이 막혀있는 구간이 존재하면 해당 구간에는 이벤트 동작이 이루어지지 않는다.

5. 캡쳐링

표준 DOM 이벤트에서 정의한 이벤트 흐름에는 3가지 단계가 있다.

캡처링 단계: 이벤트가 하위 요소로 전파되는 단계
타깃 단계: 이벤트가 실제 타깃 요소에 전달되는 단계
버블링 단계: 이벤트가 상위 요소로 전파되는 단계

타깃 단계는 이벤트 객체의 target 프로퍼티가 되는 요소에 등록되어있던 이벤트 핸들러가 동작하는 단계인데, 쉽게 생각해서 가장 처음 이벤트 핸들러가 동작하게 되는 순간이라고 생각하면 된다.

캡쳐링은 이벤트가 발생하면 가장 먼저, 그리고 버블링의 반대 방향으로 진행되는 이벤트 전파 방식이다. 보통 타깃 단계에서 target에 등록된 이벤트 핸들러가 있으면 해당 이벤트 핸들러가 먼저 동작한 이후에 버블링 단계에서 각 부모 요소에 등록된 이벤트 핸들러가 있으면 그 때 해당 이벤트 핸들러가 동작하는 것이 일반적이다.

하지만 상황에 따라서는 캡쳐링 단계에서 부모 요소의 이벤트 핸들러를 동작시켜야 할 수도 있다. 캡쳐링 단계에서 이벤트 핸들러를 동작시키려면, addEventListener에 세번째 프로퍼티에 true 또는 { capture:true }를 전달하면 된다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>Codeit Acid Rain</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div>DIV
    <ul>UL
      <li>LI</li>
    </div>
  </form>
  
  <script>
    for (let elem of document.querySelectorAll('*')) {
      elem.addEventListener("click", e => alert(`캡쳐링 단계: ${elem.tagName}`), true);
      elem.addEventListener("click", e => alert(`버블링 단계: ${elem.tagName}`));
    }
  </script>
</body>
</html>

6. 이벤트 위임

이벤트 핸들러 등록 이후 새로운 요소가 추가될경우 추가된 요소에는 이벤트가 적용되지 않는 문제가 발생한다.
이로인해 요소를 추가할 때마다 이벤트를 새로 등록해야한다는 번거로움이 있다.

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); // 이벤트가 동작하지 않음

이벤트 버블링을 사용한다면 다음과 같은 문제를 간단하게 해결할 수 있다.

list.addEventListener('click', function(e) { 
  // 부모요소인 리스트에 핸들러를 하나만 등록해주어도
  // 모든 자식 요소의 이벤트를 관리할 수 있다. 
  console.log(e.target)
  e.Target.classList.toggle('done')
});

const li = document.createElement('li');
li.classList.add('item');
li.textContent = '일기 쓰기';
list.append(li);

이처럼 자식 요소의 이벤트를 부모 요소에서 다루는 방식을 가리켜 이벤트 위임이라고 한다. 여기서 발생하는 한가지 문제점은 자식 요소가 아니라 부모요소를 클릭해도 이벤트가 발생한다는 점이다. 따라서 이벤트 위임을 사용할 때는 원하는 요소에만 이벤트가 발생할 수 있도록 따로 처리를 해주어야한다.

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);

이벤트 위임을 활용하면 새로운 자식요소를 추가하거나 삭제하더라도 이벤트에 대한 제어를 신경쓰지 않아도 되기 때문에 훨씬 더 유연하게 코드를 작성할 수 있다. 또한 여러개의 이벤트 핸들러를 만들지 않아도 된다는 점은
코드 효율 측면에서 긍정적으로 작용한다. 따라서 이벤트를 설계할 때 이벤트 위임을 우선적으로 고려하는 것이 좋다.

아래와 같이 이벤트의 버블링을 막는 메소드가 등록되어있으면
이벤트가 의도한대로 동작되지 않는다.

list.addEventListener('click', function(e) { 
	// if (e.target.tagName === 'LI')
	if (e.target.classList.contains('item')) {
		e.target.classList.toggle('done');
	}
});

li.addEventListener('click', function(e) {
   e.stopPropagation(); // 버블링을 막아 위에 작성한 이벤트 핸들러가 작동하지 않는다. 
});

7. 브라우저의 기본동작 제어 preventDefault()

브라우저는 이벤트가 발생했을 때 자동으로 실행하는 기본 동작들이 내장되어있다. (ex. 텍스트 마우스로 드래그시 하늘색으로 글자 선택, 마우스 우클릭시 옵션창 출현 등). precentDefault()메소드를 사용하면 브라우저의 기본동작을 제어할 수 있다.

const link = document.querySelector('#link');
const checkbox = document.querySelector('#checkbox');
const input = document.querySelector('#input');
const text = document.querySelector('#text');

// 브라우저의 기본 동작을 제어할 때 사용하는 메소드가 preventDefault();
link.addEventListener('click', function(e) {
	e.preventDefault();
	alert('지금은 이동할 수 없습니다.');
});

input.addEventListener('keydown', function(e) {
	if (!checkbox.checked) {
		e.preventDefault();
		alert('체크박스를 먼저 체크해 주세요.');
	}
});

document.addEventListener('contextmenu', function(e) { // 마우스 우클릭의 이벤트 타입은  'contextmenu'
	e.preventDefault();
	alert('마우스 오른쪽 클릭은 사용할 수 없습니다.');
}); // 문서 전체 우클릭 금지

좋은 웹페이지 즐겨찾기