`IntersectionObserverCallback` 함수를 등록하다가 `CusTomEvent`생성자를 사용할 일이 있었습니다. 이벤트 객체와, 커스텀 이벤트 객체에 대해서는 이전에 정확히 몰랐던 내용이라 관련 내용을 정리해두었습니다.
여러 브라우저 이벤트 종류(자주 쓰는 것들)
이벤트(event)는 무엇인가 일어났다는 신호입니다. 모든 DOM 노드는 이런 신호를 만들어 냅니다. 참고로, 이벤트는 DOM에만 한정되지 않습니다. 아래에서는 주로 사용되는 DOM 이벤트를 적어두었습니다.
1. 마우스 이벤트
click
요소 위에서 마우스 왼쪽 버튼을 눌렀을 때(터치스크린이 있는 장치에서 탭 했을 때) 발생.
contextmenu
요소 위에서 마우스 오른쪽 버튼을 눌렀을 때 발생
mouseover / mouseout
각각 마우스 커서를 위로 움직였을 때, 커서가 요소 밖으로 움직였을 때 발생.
mousemove
마우스를 움직일 때 발생
1-1. 마우스 드래그 이벤트(밑의 드래그 이벤트들은 항상 같이 다니는 한 묶음이라고 봐도 무방합니다.)
dragenter
드래그한 요소나 텍스트 블록을 적합한 드롭 대상 위에 올렸을 때 발생(드롭할 위치에 입장[도달]했을 때 맨 처음 한 번만 발생)
dragover
요소나 텍스트 블록이 적합한 드롭 대상 위를 지나갈 때 발생(매 수백 밀리초마다 발생)
dragleave
드래그하는 요소나 텍스트 블록이 적합한 드롭 대상에서 벗어났을 때 발생
drop
요소나 텍스트 블록이 적합한 드롭 대상에 드롭했을 때 발생.
🎁 input 태그에 drop 이벤트를 줄 시 새 탭에서 해당 요소가 열림.
이를 방지하려면 event.preventDefault();와 event.stopPropagation()을 해주면 됩니다. 위의 모든 드래그 이벤트에
preventDefault와 stopPropagation을 설정하면 의도하지 않은 동작(브라우저 기본 동작 새로고침, 새 탭, 버블링&캡처링 등)을 방지할 수 있습니다.
2. 폼(form) 요소 이벤트
submit
사용자가 `<form />` 을 제출할 때 발생
focus
사용자가 `<input />` 과 같은 요소에 포커스 할 때 발생
3. 키보드 이벤트
keydown / keyup
사용자가 키보드 버튼을 누르거나 뗄 때 발생. `event.key` 를 통해 어떤 특정한 키로 이벤트를 발생시켰는지 값을 얻을 수 있습니다.
4. 문서 이벤트
DOMContentLoaded
HTML이 전부 로드 및 처리되어 DOM 생성이 완료됐을 때 발생
5. CSS 이벤트
transitioned
CSS 애니메이션이 종료되었을 때 발생
이벤트 핸들러
이벤트에 반응하려면 이벤트가 발생했을 때 실행되는 함수인 핸들러(handler)를 할당해야 합니다. 핸들러는 사용자의 행동에 어떻게 반응할지를 자바스크립트 코드로 표현한 것입니다. 핸들러는 여러 가지 방법으로 할당할 수 있습니다. 본 글에서는 주로 커스텀 이벤트를 생성하고 핸들링하는 방법에 대해서 다룹니다.
이벤트 핸들러 할당 방법
1. HTML 속성
HTML 안의 on<event> 속성에 핸들러를 할당할 수 있습니다.
onClick 이후의 문자열에 () 바로 해당함수 호출문이 있다는 점을 실수하지 않게 기억해주세요.
<input value="클릭해 주세요." onclick="alert('클릭!')" type="button">
<script>
function countRabbits() {
for(let i=1; i<=3; i++) {
alert(`토끼 ${i}마리`);
}
}
</script>
<input type="button" onclick="countRabbits()" value="토끼를 세봅시다!">
또한 this로 해당 HTML요소에 접근할 수 있습니다. 아래 예시에서는 "클릭해 주세요." 글자가 담긴 alert가 뜨게 됩니다.
<button onclick="alert(this.innerHTML)">클릭해 주세요.</button>
2. DOM 프로퍼티
DOM 프로퍼티 on<event> 를 사용해도 핸들러를 할당할 수 있습니다. 핸들러를 HTML 속성을 사용해 할당하면, 브라우저는 속성값을 이용해 새로운 함수를 만듭니다. 그리고 생성된 함수를 DOM 프로퍼티에 할당합니다. 🌟 ( ) 호출문이 없다는 점 기억해주세요.
<input id="elem" type="button" value="클릭해 주세요.">
<script>
function sayThanks() {
alert('감사합니다!');
}
elem.onclick = sayThanks;
</script>
onclick 프로퍼티는 단 하나밖에 없기 때문에 복수의 핸들러를 할당할 수 없습니다.
핸들러를 하나 더 추가하면, 기존 핸들러는 덮어씌워집니다.
<input type="button" id="elem" onclick="alert('이전')" value="클릭해 주세요.">
<script>
elem.onclick = function() { // 기존에 작성된 핸들러를 덮어씀
alert('이후'); // 이 경고창('이후')만 보입니다.
};
</script>
addEventListener
Html 속성과 DOM 프로퍼티를 이용한 이벤트 핸들러 할당 방식에는 하나의 핸들러에 복수의 핸들러를 할당할 수 없다는 문제가 있습니다. 이 문제를 해결하기 위해 addEventListener가 등장했습니다.
element.addEventListener(event, handler, [options]);
핸들러 삭제는 removeEventListener로 할 수 있습니다.
element.removeEventListener(event, handler, [options]);
removeEventListener
removeEventListener를 통한 이벤트 핸들러 삭제는 핸들러 할당 시 사용한 함수를 그대로 전달해주어야 합니다. 즉, 핸들러 삭제는 동일한 함수만 할 수 있습니다. 이 때 주의할 점이 있습니다.
🚨 주의할 점은 함수 할당입니다.
함수는 참조형 데이터로서 같은 문구를 쓰더라도 다른 참조주소를 갖고 있기 때문에 새로 선언한 다른 함수로 인식하게 됩니다. 같은 문구를 적어도 주소가 다른 데이터라는 뜻이죠. 즉 아래 예시 코드는 핸들러를 지울 수 없습니다.
elem.addEventListener( "click" , () => alert('감사합니다!'));
// ....
elem.removeEventListener( "click", () => alert('감사합니다!'));
아래 코드와 같이 코드를 고쳐야 핸들러를 지울 수 있습니다.
function handler() {
alert( '감사합니다!' );
}
input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);
이벤트 객체
이벤트가 발생하면 브라우저는 이벤트 객체(event object)라는 것을 만듭니다. 여기에 이벤트에 관한 상세한 정보를 넣은 다음, 핸들러에 인수 형태로 전달합니다. 아래는 이벤트 객체로부터 포인터 좌표 정보를 얻어내는 예시입니다.
<input type="button" value="클릭해 주세요." id="elem">
<script>
elem.onclick = function(event) {
// 이벤트 타입과 요소, 클릭 이벤트가 발생한 좌표를 보여줌
alert(event.type + " 이벤트가 " + event.currentTarget + "에서 발생했습니다.");
alert("이벤트가 발생한 곳의 좌표는 " + event.clientX + ":" + event.clientY +"입니다.");
};
</script>
addEventListener 를 사용하면 함수뿐만 아니라 객체를 이벤트 핸들러로 할당할 수 있습니다. 이벤트가 발생하면 객체에 구현한 🎁 handleEvent 메서드가 호출됩니다.
// 객체 사용
<button id="elem">클릭해 주세요.</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " 이벤트가 " + event.currentTarget + "에서 발생했습니다.");
}
};
elem.addEventListener('click', obj);
</script>
// 클래스 사용
<button id="elem">클릭해 주세요.</button>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "마우스 버튼을 눌렀습니다.";
break;
case 'mouseup':
elem.innerHTML += " 그리고 버튼을 뗐습니다.";
break;
}
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventLi
커스텀 이벤트 디스패치
자바스크립트를 사용하면 핸들러를 할당할 수 있을 뿐만 아니라 이벤트를 직접 만들 수도 있습니다. 직접 만든 커스텀 이벤트(custom event)는 '그래픽 컴포넌트(graphical component)'를 만들 때 사용됩니다. 자바스크립트 기반 메뉴가 있다고 가정해 봅시다. 개발자는 메뉴의 루트 요소에 open(메뉴를 열었을 때 실행), select(항목을 선택했을 때 실행) 같은 이벤트를 달아 상황에 맞게 이벤트가 실행되게 할 수 있습니다. 이렇게 루트 요소에 이벤트 핸드러를 달아놓으면 바깥 코드에서도 커스텀 이벤트 타입에 대한 이벤트 리스닝을 통해 메뉴에서 어떤 일이 일어났는지를 파악할 수 있습니다.
자바스크립트를 사용하면 새로운 커스텀 이벤트뿐만 아니라 목적에 따라 `click`, `mousedown`과 같은 내장 이벤트를 직접 만들 수도 있습니다.
Event 생성자
내장 이벤트 클래스는 DOM 요소 클래스같이 계층 구조를 형성합니다. 내장 이벤트 클래스 계층의 꼭대기엔 Event 클래스가 있습니다. Event 객체는 다음과 같이 생성할 수 있습니다.
let event = new Event(type[, options]);
- type - 이벤트 타입(`내장(built-in) 이벤트` 혹은`my-event` 같은 커스텀 이벤트가 올 수도 있습니다.)
- options - 두 개의 선택 프로퍼티가 있는 객체가 옵니다.
- `bubbles`: true/false – true인 경우 이벤트가 버블링 됩니다.
- `cancelable`: true/false – true인 경우 후에 해당 이벤트를 콜백함수에서 사용할 때 브라우저 '기본 동작’을 실행시키지 않을 수 있습니다.(=== event.preventDefalut() 사용이 먹히게 됩니다. false일 경우 preventDefault를 사용해도 기본동작을 막을 수 없습니다.😲이전에 몰랐던 프로퍼티...)
- default는 `{bubbles: false, cancelable: false}` 입니다.
dispatchEvent⭐
이벤트 객체를 생성한 다음엔 `elem.dispatchEvent(event)` 를 호출해 요소에 있는 이벤트를 반드시 '실행'시켜줘야 합니다.(dispatch는 일을 '처리하다'라는 뜻을 가진 영단어입니다.) 이벤트를 실행시켜줘야 핸들러가 일반 브라우저 이벤트처럼 이벤트에 반응할 수 있습니다. `bubbles` 플래그를 `true` 로 해서 이벤트를 만든 경우에는 제대로 버블링됩니다.
아래 코드에서는 자바스크립트를 사용해 `click` 이벤트를 만들고 실행시켜 보았습니다. 버튼은 실제로 클릭하지 않았기만, 코드 실행만으로 이벤트 핸들러가 동작하는 것을 확인할 수 있습니다.
<button id="elem" onclick="alert('클릭!');">자동으로 클릭 되는 버튼</button>
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
커스텀 이벤트 버블링 예시
"hello"라는 이름을 가진 이벤트를 만들고 버블링 시켜서 document에서 이벤트를 처리할 수 있게 해보겠습니다.
이벤트가 버블링되게 하려면 `bubbles`를 `true`로 설정해야 합니다.
<h1 id="elem">Hello from the script!</h1>
<script>
// 버블링이 일어나면서 document에서 이벤트가 처리됨
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});
// 이벤트(hello)를 만들고 elem에서 이벤트 디스패치
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
// document에 할당된 핸들러가 동작하고 메시지가 얼럿창에 출력됩니다.
</script>
커스텀 이벤트는 반드시 addEventListener를 사용해 핸들링해야 합니다. 내장 이벤트(click)와 커스텀 이벤트(hello)의 버블링 메커니즘은 동일합니다. 이에 더하여 커스텀 이벤트에도 내장 이벤트와 마찬가지로 캡쳐링, 버블링 단계가 있습니다. (Event 객체에는 bubbles, cancelable, composed 옵션만 있네요.)
CustomEvent ⭐
위에서 new Event 로 커스텀 이벤트를 만드는 방법을 봤습니다. 하지만 제대로 된 커스텀 이벤트를 만들기 위해서는 `new CustomEvent` 를 사용해야 합니다. `CustomEvent`는 `Event`와 거의 유사하지만 한 가지 다른 점이 있습니다.
CustomEvent 의 두 번째 인수에는 객체가 들어갈 수 있는데, 개발자는 이 객체에 `detail` 이라는 프로퍼티를 추가해 커스텀 이벤트 관련 정보를 명시하고, 정보를 이벤트에 전달할 수 있습니다.
<h1 id="elem">Olimjo님, 환영합니다!</h1>
<script>
// 추가 정보는 이벤트와 함께 핸들러에 전달됩니다.
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "Olim" }
}));
</script>
이 detail 프로퍼티에는 어떠한 데이터도 들어갈 수 있습니다. 사실 new Event 로 커스텀 이벤트를 먼저 생성한 다음 추가 정보가 담긴 프로퍼티를 이벤트 객체에 추가(`event.name = "Olim"` 이런 식으로)해주면 되기 때문에 detail 프로퍼티 없이도 충분히 이벤트에 원하는 정보를 추가할 수 있긴 합니다만, 그런데도 detail 이라는 특별한 프로퍼티를 사용하는 이유는 다른 이벤트 프로퍼티와 충돌을 피하기 위해서입니다. 이 외에도 `new CustomEvent` 를 사용하면 코드 자체만으로 '커스텀 이벤트'라고 설명해주는 부가적인 효과가 있습니다.
부록
중첩 이벤트 처리하기(setTimeout을 활용한 동기적 실행을 비동기적으로 바꾸기)
먼저, 아래 코드의 작동순서를 예상해봅시다.
<button id="menu">메뉴(클릭해주세요)</button>
<script>
menu.onclick = function() {
alert(1); // (1)
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})); // (2)
alert(2); // (4)
};
// 1과 2 사이에 트리거됩니다
document.addEventListener('menu-open', () => alert('중첩 이벤트')); // (3)
</script>
위 코드에서는 'menu'라는 id를 가진 버튼을 누를 시 스크립트 안의 DOM 프로퍼티 on<event>를 사용한 onClick 이벤트가 실행됩니다. 이 때 코드의 실행 순서는 먼저 alert(1)가 실행되고 두 번째로 CustomEvent로 생성된 menu-open 이벤트 타입을 dispatchEvent 하게 되면서 'menu-open'이라는 이벤트 타입을 listen 하고 있던 이벤트 리스너 함수의 콜백인 ()=>alert('중첩 이벤트') 가 실행됩니다. 마지막으로 alert(2) 가 실행됩니다. 주목할 점은 menu-open 이벤트가 onclick 이벤트가 처리되는 도중에 트리거 됐다는 점입니다. 이벤트리스너 함수의 콜백함수가 onclick 핸들러가 끝날 때까지 기다리지 않고 바로 처리됐다는 뜻입니다. 그 이유는 버튼을 누르기 전부터 'menu open' 이벤트 타입을 리스닝하고 있던 이벤트 리스너가 있었기 때문입니다. "menu-open"이라는 이벤트가 생기기만을 기다리고 있었던 것입니다.
만약 이런 동기적 실행을 원하지 않고 이벤트 리스너의 콜백 함수가 마지막에 출력되도록 하고 싶으면 이를 비동기적으로 바꿀 필요가 있습니다. 그럴 때는 setTimeout을 통해 dispatchEvent 메서드를 비동기적으로 만들면 됩니다. 혹은 동기적으로 도 해결하고 싶다면 onclick 함수의 제일 마지막에 dispatchEvent를 놓는 방법도 있겠네요. 2 가지 방법이 있는 것이죠. 아래 코드에서는 setTimeout에 지연시간을 0초로 주었습니다.
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
alert(1); // (1)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}))); // (3)
alert(2); // (2)
};
document.addEventListener('menu-open', () => alert('중첩 이벤트')); // (4)
</script>