이벤트 드리븐 프로그래밍
브라우저는 클릭, 키보드 입력, 마우스 이동, 드래그와 같은 특정 사건이 발생하면 이를 감지하여 이벤트를 발생시킨다. 이는 사용자와 애플리케이션 간의 상호작용을 가능하게 하는 핵심 메커니즘이다.
애플리케이션에서 특정 이벤트에 반응하여 어떤 동작을 수행하고 싶다면, 해당 이벤트가 발생했을 때 실행될 함수를 브라우저에게 알려주어 호출을 위임해야 한다. 이벤트의 발생 시점을 개발자가 예측할 수 없기 때문에, 함수의 호출 시점을 브라우저에게 위임하는 방식을 사용하는 것이다. 이는 이벤트 기반 프로그래밍의 기본적인 동작 방식이다.
이러한 메커니즘에서 핵심이 되는 두 가지 개념이 있다.
- 이벤트 핸들러: 이벤트가 발생했을 때 호출될 함수를 말한다. 이벤트에 대한 처리를 담당하며, 이벤트 발생 시 브라우저에 의해 자동으로 호출된다.
- 이벤트 핸들러 등록: 특정 이벤트가 발생했을 때 실행될 이벤트 핸들러를 브라우저에게 알려주는 과정이다. 이를 통해 브라우저는 해당 이벤트 발생 시 어떤 함수를 호출해야 할지 알 수 있다.
예를 들어, 버튼 클릭에 대한 이벤트 핸들러를 등록하는 코드는 다음과 같다.
const $button = document.querySelector('button');
// 버튼 클릭 시 실행될 이벤트 핸들러를 등록한다
$button.onclick = () => {
alert('button click');
}
브라우저의 Window, Document, HTMLElement와 같은 객체들은 다양한 이벤트 핸들러 프로퍼티를 내장하고 있다. 이러한 프로퍼티들을 통해 개발자는 원하는 이벤트에 대한 핸들러를 쉽게 등록할 수 있다. 각각의 객체는 자신이 발생시킬 수 있는 이벤트에 대한 핸들러 프로퍼티를 가지고 있으며, 이를 통해 이벤트와 핸들러 함수를 연결할 수 있다.
이벤트 드리븐 프로그래밍은 이러한 이벤트와 이벤트 핸들러를 기반으로 프로그램의 흐름을 제어하는 프로그래밍 방식이다. 이는 사용자의 동작이나 시스템의 상태 변화에 반응하여 동작하는 대부분의 현대적인 웹 애플리케이션에서 핵심적인 프로그래밍 패러다임으로 자리잡았다.
이벤트 타입
이벤트 타입은 발생한 이벤트의 종류를 식별하는 문자열이다. 브라우저는 약 200여 가지의 다양한 이벤트 타입을 지원하며, 각각의 이벤트는 특정한 상황이나 동작을 나타낸다. 이벤트 타입에 대한 자세한 정보는 MDN의 Event reference에서 확인할 수 있다.
마우스 이벤트
- click: 마우스 버튼을 클릭했을 때 발생한다. 가장 기본적이고 많이 사용되는 이벤트이다.
- dblclick: 마우스 버튼을 빠르게 두 번 연속 클릭했을 때 발생한다.
- mousedown: 마우스 버튼을 누르는 순간에 발생한다. 버튼을 누르고 있는 상태를 감지할 때 사용한다.
- mouseup: 눌려있던 마우스 버튼을 놓는 순간에 발생한다. 드래그 앤 드롭 등의 기능을 구현할 때 자주 사용된다.
- mousemove: 마우스 커서를 움직일 때마다 발생한다. 마우스의 움직임을 추적해야 할 때 사용한다.
- mouseenter: 마우스 커서가 HTML 요소의 경계 안으로 들어올 때 발생한다. 버블링이 발생하지 않아 정확한 요소의 진입을 감지할 수 있다.
- mouseover: mouseenter와 비슷하지만 버블링이 발생한다. 부모 요소에도 이벤트가 전파되어야 할 때 사용한다.
- mouseleave: 마우스 커서가 HTML 요소의 경계를 벗어날 때 발생한다. 버블링이 발생하지 않는다.
- mouseout: mouseleave와 비슷하지만 버블링이 발생한다.
여기서 버블링은 중요한 개념이다. 버블링이란 특정 요소에서 발생한 이벤트가 그 요소의 부모 요소들로 계속해서 전파되는 현상을 말한다.
예를 들어, 버블링이 있는 mouseover 이벤트의 경우 자식 요소에서 발생한 이벤트가 부모 요소들에게도 전달되지만, 버블링이 없는 mouseenter의 경우 해당 요소에서만 이벤트가 발생하고 부모로 전파되지 않는다.
키보드 이벤트
키보드 이벤트는 사용자의 키보드 입력을 감지하는 이벤트이다. 키보드 이벤트는 다음과 같은 특징을 가진다.
- keydown: 키보드의 모든 키를 눌렀을 때 발생하는 이벤트이다. 키의 종류에 따라 동작이 다른데:
- 문자, 숫자, 특수 문자, Enter 키는 키를 누르고 있는 동안 연속적으로 발생한다.
- Control, Option, Shift, Tab, Delete, 방향 키 등의 특수 키는 처음 눌렀을 때 한 번만 발생한다.
- 이는 일반적인 텍스트 입력과 특수 키 입력을 구분하여 처리할 수 있게 해준다.
- keypress: 문자 키를 눌렀을 때만 발생하는 이벤트이다. 하지만 이 이벤트는 더 이상 사용이 권장되지 않는다(deprecated). 대신 keydown 이벤트를 사용하는 것이 좋다.
- keyup: 누르고 있던 키를 놓았을 때 한 번만 발생하는 이벤트이다. 모든 키에 대해 동일하게 동작하며, 키 입력의 완료를 감지하는데 유용하다.
포커스 이벤트
포커스 이벤트는 HTML 요소가 포커스를 받거나 잃을 때 발생하는 이벤트이다. 포커스는 현재 사용자의 입력을 받을 수 있는 상태를 의미하며, 특히 폼 요소에서 중요하다.
- focus: HTML 요소가 포커스를 받았을 때 발생한다. 버블링이 발생하지 않아 정확한 대상 요소에서만 이벤트가 발생한다.
- blur: HTML 요소가 포커스를 잃었을 때 발생한다. focus와 마찬가지로 버블링이 발생하지 않는다.
- focusin: focus와 같은 상황에서 발생하지만, 버블링이 발생하여 부모 요소들에게도 이벤트가 전파된다.
- focusout: blur와 같은 상황에서 발생하지만, 버블링이 발생한다.
중요한 주의사항으로, focusin과 focusout 이벤트를 이벤트 핸들러 프로퍼티 방식(예: element.onfocusin)으로 등록하면 크롬과 사파리 브라우저에서 정상적으로 동작하지 않는다.
// 올바른 방법
element.addEventListener('focusin', () => {
console.log('포커스 획득');
});
// 동작하지 않을 수 있는 방법
element.onfocusin = () => {
console.log('포커스 획득');
};
따라서 이 두 이벤트는 반드시 addEventListener 메서드를 사용하여 등록해야 한다.
폼 이벤트
폼 이벤트는 HTML form 요소와 관련된 이벤트를 다룬다. 주로 데이터 제출과 관련이 있으며, 다음과 같은 이벤트들이 있다.
- submit: form 요소에서 발생하는 이벤트로, 다음 두 가지 상황에서 발생한다.
- form 요소 내의 input, select 등의 입력 필드에서 Enter 키를 눌렀을 때
- form 요소 내의 submit 버튼(button type="submit" 또는 input type="submit")을 클릭했을 때
- reset: form 요소 내의 reset 버튼을 클릭했을 때 발생한다. 하지만 최근의 웹 개발에서는 거의 사용되지 않는다. 이는 사용자 경험(UX) 측면에서 form 초기화 버튼이 실수로 클릭되어 입력한 데이터가 모두 사라질 수 있는 위험이 있기 때문이다.
값 변경 이벤트
값 변경 이벤트는 HTML 요소의 값이 변경될 때 발생하는 이벤트들이다.
- input: input(text, checkbox, radio), select, textarea 요소의 값이 입력되는 중에 발생한다.
- 사용자가 값을 입력할 때마다 실시간으로 발생하므로, 입력값의 실시간 유효성 검사나 자동완성 기능 등을 구현할 때 유용하다.
- 모든 입력 방식(키보드, 마우스, 드래그앤드롭, 음성인식 등)에 대해 동작한다.
- change: input(text, checkbox, radio), select, textarea 요소의 값이 변경되고 그 변경이 완료되었을 때 발생한다.
- input 이벤트와 달리, 값이 변경되고 요소가 포커스를 잃었을 때(blur) 발생한다.
- 예를 들어, 텍스트 입력의 경우 사용자가 입력을 마치고 다른 요소를 클릭하거나 Tab 키를 눌러 포커스를 이동할 때 발생한다.
- checkbox나 radio의 경우 선택이 변경되는 즉시 발생한다.
- readystatechange: HTML 문서의 로드와 파싱 상태를 나타내는 document.readyState 프로퍼티 값이 변경될 때 발생한다.
- readyState 값은 다음 세 가지가 있다.
- 'loading': 문서를 불러오는 중
- 'interactive': 문서의 파싱이 완료되어 DOM 접근이 가능한 상태
- 'complete': 문서와 모든 리소스의 로딩이 완료된 상태
- readyState 값은 다음 세 가지가 있다.
DOM 뮤테이션 이벤트
DOM 뮤테이션 이벤트는 DOM의 변경과 관련된 이벤트를 다룬다.
- DOMContentLoaded: HTML 문서의 로드와 파싱이 완료되어 DOM 생성이 완료되었을 때 발생하는 이벤트이다. 이 이벤트는 HTML 문서가 완전히 로드되고 DOM 트리가 완성된 시점을 나타내며, 외부 리소스(이미지, 스타일시트, 스크립트 등)의 로드가 완료되지 않았더라도 DOM이 준비되면 발생한다. 이는 JavaScript가 DOM을 조작하기 시작할 수 있는 가장 빠른 시점을 알려주므로, 초기화 코드를 실행하기에 적합한 시점이다.
뷰 이벤트
뷰 이벤트는 사용자의 화면(viewport)과 관련된 변경사항을 감지하는 이벤트들이다.
- resize: 브라우저 윈도우의 크기가 변경될 때 연속적으로 발생하는 이벤트이다.
- 이 이벤트는 오직 window 객체에서만 발생한다.
- 사용자가 브라우저 창의 크기를 조절하는 동안 계속해서 발생하므로, 성능을 고려하여 디바운싱이나 쓰로틀링 처리가 필요할 수 있다.
- 반응형 웹 디자인에서 화면 크기에 따른 동적인 레이아웃 조정이 필요한 경우 유용하게 사용된다.
- scroll: 웹페이지나 HTML 요소의 스크롤이 발생할 때 연속적으로 발생하는 이벤트이다.
- document 객체나 스크롤 가능한 HTML 요소에서 발생한다.
- resize 이벤트와 마찬가지로 연속적으로 발생하므로, 성능 최적화를 위한 처리가 필요할 수 있다.
- 무한 스크롤, 스크롤 위치에 따른 애니메이션, 스크롤 프로그레스 바 등의 기능을 구현할 때 사용된다.
리소스 이벤트
리소스 이벤트는 웹페이지의 리소스 로딩 상태와 관련된 이벤트들이다.
- load: DOMContentLoaded 이벤트 발생 이후, 웹페이지의 모든 리소스(이미지, 스타일시트, 스크립트, 폰트 등)의 로딩이 완료되었을 때 발생하는 이벤트이다.
- 주로 window 객체에서 발생한다.
- 페이지의 모든 콘텐츠가 완전히 로드된 시점을 알 수 있다.
- 이미지나 iframe과 같은 개별 리소스 요소에서도 발생할 수 있다.
- unload: 현재 문서에서 다른 문서로 이동하거나 브라우저 창을 닫을 때와 같이 리소스가 언로드될 때 발생하는 이벤트이다.
- 주로 새로운 페이지로 이동하거나 브라우저를 닫을 때 발생한다.
- 페이지를 떠나기 전에 마지막으로 처리해야 할 작업이 있을 때 사용할 수 있다.
- 단, 현대의 웹 브라우저에서는 성능과 사용자 경험 향상을 위해 unload 이벤트의 사용을 제한하는 경우가 있다.
- abort: 리소스 로딩이 사용자에 의해 중단되었거나 오류로 인해 중단되었을 때 발생하는 이벤트이다.
- 이미지나 비디오와 같은 미디어 요소의 로딩이 중단되었을 때 주로 발생한다.
- 네트워크 문제나 사용자의 취소 동작으로 인한 로딩 실패를 처리할 때 사용된다.
- error: 리소스 로딩이 실패했을 때 발생하는 이벤트이다.
- 404 에러나 네트워크 오류 등으로 인해 리소스를 불러오지 못했을 때 발생한다.
- 이미지나 스크립트와 같은 외부 리소스 로딩 실패를 감지하고 대체 콘텐츠를 제공하거나 오류 메시지를 표시하는 등의 처리를 할 때 사용된다.
- JavaScript 코드의 실행 중 발생하는 에러도 window 객체의 error 이벤트로 캐치할 수 있다.
이벤트 핸들러 등록
이벤트 핸들러는 브라우저에서 특정 이벤트가 발생했을 때 실행되는 함수를 말한다. 브라우저는 클릭, 키보드 입력, 마우스 이동과 같은 특정한 사건이 발생하면 이를 감지하여 이벤트를 발생시키고, 그에 대응하는 이벤트 핸들러를 호출한다. 개발자는 이벤트의 발생 시점을 알 수 없기 때문에, 직접 함수를 호출하는 것이 아니라 브라우저에게 함수의 호출을 위임하는 방식을 사용한다. 이렇게 이벤트가 발생했을 때 브라우저에 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라고 한다.
이벤트 핸들러를 등록하는 방법은 크게 세 가지가 있으며, 각각의 방법은 자신만의 특징과 장단점을 가지고 있다. 이벤트 핸들러는 대개 이벤트를 발생시킬 타깃에 바인딩하지만, 반드시 그래야 하는 것은 아니다. 이벤트 위임 패턴을 사용할 때는 상위 DOM 요소에 이벤트 핸들러를 바인딩하기도 한다.
이벤트 핸들러 어트리뷰트 방식
이벤트 핸들러 어트리뷰트 방식은 HTML 요소의 어트리뷰트를 통해 직접 이벤트 핸들러를 등록하는 가장 오래된 방식이다. 모든 HTML 요소는 이벤트에 대응하는 이벤트 핸들러 어트리뷰트를 가지고 있다. 이 어트리뷰트의 이름은 'on'으로 시작하며, 그 뒤에 이벤트의 종류를 나타내는 이벤트 타입이 붙는 형태를 가진다. 예를 들어, 클릭 이벤트의 경우 'onclick', 키보드 입력 이벤트의 경우 'onkeyup'과 같은 형태다.
<button onclick="sayHi('Lee'); displayMessage();">Click me!</button>
<script>
function sayHi(name) {
console.log(`Hi! ${name}.`);
}
function displayMessage() {
console.log('Button was clicked!');
}
</script>
이 방식에서 특별히 주의해야 할 점은 어트리뷰트 값으로 함수 참조가 아닌 함수 호출문을 포함한 문을 할당한다는 것이다. 이는 이벤트 핸들러 어트리뷰트 값이 암묵적으로 생성되는 이벤트 핸들러의 함수 몸체를 구성하기 때문이다. 위의 예제에서 브라우저는 다음과 같은 함수를 암묵적으로 생성한다.
function onclick(event) {
sayHi('Lee');
displayMessage();
}
이러한 동작 방식은 이벤트 핸들러에 인수를 전달할 필요가 있을 때 특히 유용하다. 만약 단순히 함수 참조만을 할당할 수 있다면 인수를 전달하는 것이 매우 번거로워질 것이다.
이벤트 핸들러 어트리뷰트 방식은 오래된 코드에서 흔히 볼 수 있지만, 현대의 웹 개발에서는 일반적으로 권장되지 않는다. 이는 HTML과 자바스크립트가 혼재되어 관심사 분리(Separation of Concerns)가 제대로 이루어지지 않기 때문이다.
하지만 흥미로운 점은 React, Vue, Angular와 같은 현대적인 프레임워크/라이브러리에서는 이와 유사한 방식을 사용한다는 것이다. 이는 이러한 프레임워크들이 컴포넌트 기반 개발(CBD, Component Based Development) 방식을 채택하고 있기 때문이다. CBD에서는 HTML, CSS, 자바스크립트를 개별적인 요소가 아닌 하나의 컴포넌트를 구성하는 통합된 요소로 보기 때문에, 이벤트 핸들러를 HTML과 유사한 방식으로 작성하는 것이 자연스럽다고 본다.
// React의 경우
<button onClick={handleClick}>Click me!</button>
이벤트 핸들러 프로퍼티 방식
이벤트 핸들러 프로퍼티 방식은 DOM 노드 객체의 프로퍼티를 통해 이벤트 핸들러를 등록하는 방식이다. window 객체, Document 객체, HTMLElement 타입의 DOM 노드 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있는데, 이 프로퍼티에 함수를 할당함으로써 이벤트 핸들러를 등록할 수 있다.
이벤트 핸들러 프로퍼티의 키는 이벤트 핸들러 어트리뷰트와 마찬가지로 'on' 접두사와 이벤트 타입으로 이루어져 있다.
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티에 함수 할당
$button.onclick = function() {
console.log('Button clicked!');
this.style.backgroundColor = 'red';
};
이 방식의 가장 큰 장점은 HTML과 자바스크립트가 분리되어 있다는 것이다. 이는 관심사 분리의 원칙에 부합하며, 코드의 유지보수성을 높여준다. 이벤트 핸들러 프로퍼티 방식을 사용하기 위해서는 세 가지 요소가 필요하다.
- 이벤트를 발생시킬 객체인 이벤트 타깃
- 이벤트의 종류를 나타내는 문자열인 이벤트 타입
- 이벤트가 발생했을 때 실행될 이벤트 핸들러
이벤트 핸들러는 일반적으로 이벤트 타깃에 바인딩하지만, 반드시 그럴 필요는 없다. 이벤트 위임 패턴을 사용할 때는 이벤트 타깃의 상위 DOM 요소에 이벤트 핸들러를 바인딩하기도 한다. 이는 이벤트가 DOM 트리를 통해 전파되는 특성을 활용한 것이다.
그러나 이 방식에는 중요한 단점이 있다. 하나의 이벤트에 대해 하나의 이벤트 핸들러만 바인딩할 수 있다는 것이다.
$button.onclick = function() {
console.log('First handler'); // 이 핸들러는 실행되지 않는다
};
$button.onclick = function() {
console.log('Second handler'); // 이 핸들러만 실행된다
};
두 번째로 할당한 이벤트 핸들러가 첫 번째 이벤트 핸들러를 덮어쓰게 되어, 결과적으로 하나의 이벤트에 대해 여러 개의 핸들러를 등록할 수 없다는 제약이 있다.
addEventListener 메서드 방식
DOM Level 2에서 도입된 EventTarget.prototype.addEventListener 메서드는 이벤트 핸들러를 등록하는 가장 현대적이고 유연한 방식이다. 이 메서드는 앞서 살펴본 두 방식의 단점을 보완하며, 더욱 세밀한 이벤트 제어를 가능하게 한다. 기본적인 구문은 다음과 같다.
EventTarget.addEventListener('eventType', handler [, useCapture]);
첫 번째 매개변수는 이벤트의 종류를 나타내는 문자열인 이벤트 타입을 전달하는데, 이때 'on' 접두사를 붙이지 않는다. 두 번째 매개변수는 이벤트 핸들러 함수이며, 마지막 매개변수는 이벤트 전파 단계를 지정하는 불리언 값이다. 이 마지막 매개변수는 생략 가능하며, 기본값은 false다.
const $button = document.querySelector('button');
// 이벤트 핸들러 등록
$button.addEventListener('click', function() {
console.log('Button clicked!');
this.style.backgroundColor = 'red';
});
addEventListener 메서드의 가장 큰 특징은 하나의 이벤트에 대해 여러 개의 이벤트 핸들러를 등록할 수 있다는 것이다. 등록된 이벤트 핸들러들은 등록된 순서대로 실행된다.
const $button = document.querySelector('button');
$button.addEventListener('click', function() {
console.log('첫 번째 이벤트 핸들러');
});
$button.addEventListener('click', function() {
console.log('두 번째 이벤트 핸들러');
});
// 버튼 클릭 시 두 메시지가 순서대로 출력된다
또한 addEventListener 메서드는 이벤트 핸들러 프로퍼티 방식과 함께 사용할 수 있다. 두 방식으로 등록된 이벤트 핸들러들은 서로 영향을 주지 않고 독립적으로 동작한다.
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function() {
console.log('프로퍼티 방식의 핸들러');
};
// addEventListener 메서드 방식
$button.addEventListener('click', function() {
console.log('addEventListener 방식의 핸들러');
});
// 버튼 클릭 시 두 메시지가 모두 출력된다
한 가지 주의할 점은 동일한 이벤트 핸들러를 중복해서 등록하는 경우다. addEventListener 메서드는 동일한 이벤트 타입과 동일한 이벤트 핸들러에 대해 중복 등록을 허용하지 않는다.
const $button = document.querySelector('button');
const handleClick = () => console.log('Button clicked!');
// 동일한 이벤트 핸들러를 중복 등록
$button.addEventListener('click', handleClick);
$button.addEventListener('click', handleClick);
// 버튼 클릭 시 한 번만 실행된다
이벤트 핸들러 제거
이벤트 핸들러를 제거하는 방법은 등록 방식에 따라 다르다. addEventListener 메서드로 등록한 이벤트 핸들러를 제거하려면 EventTarget.prototype.removeEventListener 메서드를 사용해야 한다. 이때 주의할 점은 addEventListener 메서드에 전달한 인수와 완전히 동일한 인수를 removeEventListener 메서드에게 전달해야 한다는 것이다.
const $button = document.querySelector('button');
const handleClick = () => console.log('Button clicked!');
// 이벤트 핸들러 등록
$button.addEventListener('click', handleClick);
// 이벤트 핸들러 제거 - 성공
$button.removeEventListener('click', handleClick);
// 이벤트 핸들러 제거 - 실패 (매개변수가 일치하지 않음)
$button.removeEventListener('click', handleClick, true);
무명 함수로 등록한 이벤트 핸들러는 제거할 수 없다는 점도 중요하다. 이는 이벤트 핸들러를 제거하려면 함수의 참조를 어딘가에 저장하고 있어야 하기 때문이다.
// 제거할 수 없는 이벤트 핸들러
$button.addEventListener('click', () => console.log('이벤트 발생'));
// 제거할 수 있는 이벤트 핸들러
const handleClick = () => console.log('이벤트 발생');
$button.addEventListener('click', handleClick);
한편, 이벤트 핸들러를 단 한 번만 실행하고 싶은 경우가 있다. 이런 경우에는 이벤트 핸들러 내부에서 removeEventListener 메서드를 호출하여 자신을 제거하는 방법을 사용할 수 있다.
$button.addEventListener('click', function foo() {
console.log('이벤트 발생');
// 이벤트 핸들러를 제거하여 단 한 번만 실행되도록 한다
$button.removeEventListener('click', foo);
});
마지막으로, 이벤트 핸들러 프로퍼티 방식으로 등록한 이벤트 핸들러는 removeEventListener 메서드로 제거할 수 없다. 대신 이벤트 핸들러 프로퍼티에 null을 할당하여 제거할 수 있다.
const $button = document.querySelector('button');
$button.onclick = () => console.log('이벤트 발생');
// 이벤트 핸들러 제거
$button.onclick = null;
이렇게 등록한 방식에 따라 적절한 제거 방법을 사용하는 것이 중요하다. 특히 메모리 누수를 방지하고 성능을 최적화하기 위해서는 더 이상 필요하지 않은 이벤트 핸들러를 적절히 제거하는 것이 좋다.
이벤트 객체
이벤트 객체는 이벤트가 발생할 때 생성되는 객체로, 해당 이벤트에 대한 다양한 정보를 담고 있다. 이벤트가 발생하면 브라우저는 이벤트 객체를 동적으로 생성하고, 이를 이벤트 핸들러의 첫 번째 인수로 전달한다.
이벤트 객체를 전달받는 방식은 이벤트 핸들러를 등록한 방식에 따라 다르다. addEventListener 메서드나 이벤트 핸들러 프로퍼티 방식으로 등록한 경우, 이벤트 핸들러의 첫 번째 매개변수로 이벤트 객체를 자유롭게 전달받을 수 있다. 매개변수의 이름은 관례적으로 'e' 또는 'event'를 사용하지만, 어떤 이름을 사용해도 상관없다.
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function(e) {
console.log(e.type); // 'click'
};
// addEventListener 방식
$button.addEventListener('click', function(event) {
console.log(event.type); // 'click'
});
반면, 이벤트 핸들러 어트리뷰트 방식을 사용하는 경우에는 반드시 'event'라는 이름을 사용해야 한다. 이는 이벤트 핸들러 어트리뷰트 값이 암묵적으로 생성하는 이벤트 핸들러 함수의 첫 번째 매개변수 이름이 'event'로 고정되어 있기 때문이다.
<button onclick="handleClick(event)">Click me!</button>
<script>
function handleClick(event) {
console.log(event.type); // 'click'
}
</script>
이벤트 객체의 상속 구조
이벤트 객체는 발생한 이벤트의 타입에 따라 다양한 타입의 객체가 생성된다. 이러한 이벤트 객체들은 상속 구조를 통해 계층적으로 구성되어 있다. 모든 이벤트 객체는 Event 인터페이스를 상속받으며, 이벤트의 종류에 따라 다음과 같은 하위 인터페이스들이 존재한다.
- Event: 모든 이벤트 객체의 기본이 되는 인터페이스
- UIEvent: 사용자 인터페이스와 관련된 이벤트 객체의 인터페이스
- MouseEvent: 마우스 관련 이벤트 객체
- KeyboardEvent: 키보드 관련 이벤트 객체
- InputEvent: input 요소의 값 변경 관련 이벤트 객체
- FocusEvent: 포커스 관련 이벤트 객체
- CustomEvent: 개발자가 직접 정의하는 커스텀 이벤트 객체
- UIEvent: 사용자 인터페이스와 관련된 이벤트 객체의 인터페이스
예를 들어, 마우스 클릭 이벤트가 발생하면 MouseEvent 타입의 이벤트 객체가 생성되며, 이 객체는 UIEvent와 Event 인터페이스를 상속받아 해당 인터페이스들의 모든 프로퍼티와 메서드를 사용할 수 있다.
이벤트 객체의 공통 프로퍼티
Event 인터페이스는 모든 이벤트 객체가 공통으로 가지는 프로퍼티를 정의한다. 이러한 공통 프로퍼티들은 이벤트의 기본적인 정보를 제공한다.
- type: 발생한 이벤트의 타입을 나타내는 문자열이다.
- target: 이벤트가 실제로 발생한 DOM 요소를 가리킨다.
- currentTarget: 이벤트 핸들러가 바인딩된 DOM 요소를 가리킨다.
- eventPhase: 이벤트 전파의 현재 단계를 나타내는 정수값이다.
- 0: 이벤트 없음
- 1: 캡처링 단계
- 2: 타깃 단계
- 3: 버블링 단계
- bubbles: 이벤트가 버블링으로 전파되는지 여부를 나타내는 불리언 값이다. 다음 이벤트들은 버블링이 발생하지 않는다(bubbles: false):
- 포커스 이벤트: focus, blur
- 리소스 이벤트: load, unload, abort, error
- 마우스 이벤트: mouseenter, mouseleave
- cancelable: preventDefault 메서드를 호출하여 이벤트의 기본 동작을 취소할 수 있는지 여부를 나타내는 불리언 값이다.
- defaultPrevented: preventDefault 메서드를 호출하여 이벤트의 기본 동작이 취소되었는지를 나타내는 불리언 값이다.
- isTrusted: 사용자의 행위에 의해 발생한 이벤트인지 여부를 나타내는 불리언 값이다. 예를 들어, click 메서드를 통해 프로그래밍적으로 발생시킨 이벤트인 경우 false를 반환한다.
- timeStamp: 이벤트가 발생한 시각을 나타내며, 1970년 1월 1일 00:00:00부터 경과한 밀리초를 반환한다.
예를 들어, 체크박스의 체크 상태가 변경되었을 때 이벤트 객체의 공통 프로퍼티를 활용하는 예제를 살펴보자.
const $checkbox = document.querySelector('input[type=checkbox]');
const $msg = document.querySelector('.message');
$checkbox.onchange = e => {
// e.target은 이벤트가 발생한 요소인 checkbox를 가리킨다
// e.currentTarget도 동일한 요소를 가리킨다
console.log(e.target === e.currentTarget); // true
// 체크 상태를 메시지로 출력한다
$msg.textContent = e.target.checked ? 'ON' : 'OFF';
};
마우스 정보 취득
MouseEvent 타입의 이벤트 객체는 마우스 이벤트와 관련된 다양한 정보를 제공하는 고유한 프로퍼티를 가지고 있다. 이러한 이벤트에는 click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave 등이 있다.
마우스 이벤트 객체의 프로퍼티는 크게 마우스 포인터의 좌표 정보와 버튼 정보로 나눌 수 있다.
마우스 포인터 좌표 정보
- screenX/screenY: 사용자의 모니터 화면을 기준으로 한 좌표
- clientX/clientY: 브라우저의 뷰포트(웹페이지의 가시 영역)를 기준으로 한 좌표
- pageX/pageY: 전체 문서를 기준으로 한 좌표로, 스크롤이 있는 경우 이를 포함한다
- offsetX/offsetY: 이벤트가 발생한 요소를 기준으로 한 좌표
버튼 정보
- altKey: Alt 키를 누른 상태인지 여부
- ctrlKey: Ctrl 키를 누른 상태인지 여부
- shiftKey: Shift 키를 누른 상태인지 여부
- button: 누른 마우스 버튼을 나타내는 정수값
- 0: 마우스 왼쪽 버튼
- 1: 마우스 휠
- 2: 마우스 오른쪽 버튼
const $box = document.querySelector('.box');
// 드래그 시작 시점의 마우스 포인터 위치
const initialMousePos = { x: 0, y: 0 };
// 요소의 이동 거리
const offset = { x: 0, y: 0 };
// 드래그 시작
$box.addEventListener('mousedown', e => {
// 이동 거리 계산을 위해 드래그 시작 시점의 마우스 포인터 위치 저장
initialMousePos.x = e.clientX - offset.x;
initialMousePos.y = e.clientY - offset.y;
// mousedown 상태에서 mousemove 이벤트 발생 시 요소 이동
document.addEventListener('mousemove', move);
});
// 드래그 중
function move(e) {
// 이동 거리 계산
offset.x = e.clientX - initialMousePos.x;
offset.y = e.clientY - initialMousePos.y;
// 요소 이동
$box.style.transform = `translate3d(${offset.x}px, ${offset.y}px, 0)`;
}
// 드래그 종료
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', move);
});
예를 들어, 위처럼 요소를 드래그하여 이동시키는 기능을 구현할 때 이러한 마우스 정보를 활용할 수 있다.
키보드 정보 취득
KeyboardEvent 타입의 이벤트 객체는 키보드 이벤트와 관련된 정보를 제공한다. 키보드 이벤트에는 keydown, keyup, keypress(deprecated) 등이 있다.
KeyboardEvent 객체는 다음과 같은 고유한 프로퍼티를 제공한다.
- key: 입력한 키의 값을 문자열로 반환한다. 예를 들어 'Enter', 'a', 'A', '1' 등이다.
- keyCode: 입력한 키의 ASCII 코드 값을 반환하지만, 이는 더 이상 사용이 권장되지 않는다.
- altKey: Alt 키를 누른 상태인지를 나타내는 불리언 값이다.
- ctrlKey: Ctrl 키를 누른 상태인지를 나타내는 불리언 값이다.
- shiftKey: Shift 키를 누른 상태인지를 나타내는 불리언 값이다.
- metaKey: Meta 키(Windows 키 또는 Mac의 Command 키)를 누른 상태인지를 나타내는 불리언 값이다.
const $input = document.querySelector('input[type="text"]');
const $msg = document.querySelector('.message');
$input.addEventListener('keyup', e => {
// 엔터 키가 아닌 경우 무시
if (e.key !== 'Enter') return;
// 현재까지 입력된 값을 출력
$msg.textContent = e.target.value;
// 입력 필드 초기화
e.target.value = '';
});
예를 들어, input 요소에서 엔터 키를 눌렀을 때 입력된 값을 처리하는 위의 예제를 살펴보자.
$input.addEventListener('keyup', e => {
// 조합 중이거나 엔터 키가 아닌 경우 무시
if (e.isComposing || e.key !== 'Enter') return;
$msg.textContent = e.target.value;
e.target.value = '';
});
여기서 주의할 점은 한글과 같은 조합문자를 입력할 때는 keyup 이벤트가 두 번 발생할 수 있다는 것이다. 이를 방지하기 위해서는 isComposing 프로퍼티를 체크해야 한다.
이벤트 전파
DOM 트리 상에서 이벤트가 발생하면, 해당 이벤트는 DOM 트리를 따라 전파된다. 이를 이벤트 전파(Event Propagation)라고 하며, 전파 과정은 세 단계로 구분된다.
예를 들어 다음과 같은 HTML 구조가 있다고 가정해보자.
<div class="container">
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</div>
위 구조에서 'banana' 요소를 클릭하면 이벤트는 다음과 같은 세 단계로 전파된다.
- 캡처링 단계(Capturing Phase): 이벤트가 window에서 시작하여 이벤트 타깃의 부모 요소들을 거쳐 내려가는 단계다. 위 예제에서는 window → document → html → body → div → ul → li 순으로 전파된다.
- 타깃 단계(Target Phase): 이벤트가 실제 타깃 요소에 도달한 단계다. 여기서는 'banana' li 요소가 된다.
- 버블링 단계(Bubbling Phase): 이벤트가 타깃 요소에서 시작하여 상위 요소들을 거쳐 window까지 올라가는 단계다. li → ul → div → body → html → document → window 순으로 전파된다.
이벤트 핸들러는 등록 방식에 따라 다른 단계의 이벤트를 캐치할 수 있다.
- 이벤트 핸들러 어트리뷰트 방식과 프로퍼티 방식: 타깃 단계와 버블링 단계의 이벤트만 캐치할 수 있다.
- addEventListener 메서드 방식: 세 번째 인수에 따라 모든 전파 단계의 이벤트를 선택적으로 캐치할 수 있다.
const $fruits = document.getElementById('fruits');
const $banana = document.getElementById('banana');
// 캡처링 단계의 이벤트 캐치
$fruits.addEventListener('click', e => {
console.log('캡처링 단계:', e.target.id);
}, true);
// 타깃 단계의 이벤트 캐치
$banana.addEventListener('click', e => {
console.log('타깃 단계:', e.target.id);
});
// 버블링 단계의 이벤트 캐치
$fruits.addEventListener('click', e => {
console.log('버블링 단계:', e.target.id);
});
이러한 이벤트 전파 특성을 이용하면, 여러 개의 하위 요소에 각각 이벤트 핸들러를 등록하는 대신 상위 요소 하나에만 이벤트 핸들러를 등록하여 하위 요소들의 이벤트를 처리할 수 있다. 이를 이벤트 위임(Event Delegation)이라고 하며, 다음 절에서 자세히 다룬다.
단, 모든 이벤트가 버블링되는 것은 아니다. focus/blur, load/unload/abort/error, mouseenter/mouseleave 같은 이벤트는 버블링되지 않는다. 이러한 이벤트를 상위 요소에서 캐치하려면 캡처링 단계에서 이벤트를 캐치하거나, 버블링되는 유사한 이벤트(focusin/focusout, mouseover/mouseout)를 사용해야 한다.
이벤트 위임
이벤트 위임(Event Delegation)은 DOM 요소의 이벤트 처리를 최적화하는 중요한 프로그래밍 패턴이다. 이는 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신, 하나의 상위 DOM 요소에만 이벤트 핸들러를 등록하여 하위 요소들의 이벤트를 처리하는 방식이다.
이벤트 위임이 필요한 이유는 다음과 같다. 예를 들어, 내비게이션 메뉴의 각 항목(li 요소)을 클릭했을 때 특정 동작을 수행해야 하는 경우를 생각해보자.
<nav>
<ul id="menu">
<li id="home">Home</li>
<li id="about">About</li>
<li id="contact">Contact</li>
<!-- 수십 개의 메뉴 항목이 더 있을 수 있다 -->
</ul>
</nav>
각각의 li 요소에 이벤트 핸들러를 개별적으로 등록하는 접근 방식은 다음과 같은 문제를 야기할 수 있다.
- 성능 저하: 많은 수의 이벤트 핸들러가 메모리에 등록되어야 한다.
- 유지보수의 어려움: 새로운 메뉴 항목이 추가될 때마다 이벤트 핸들러를 추가로 등록해야 한다.
- 동적 요소 처리의 복잡성: JavaScript로 새로운 메뉴 항목을 동적으로 추가할 경우, 해당 요소에 대한 이벤트 핸들러 등록을 별도로 처리해야 한다.
이벤트 위임을 사용하면 이러한 문제들을 효과적으로 해결할 수 있다.
const $menu = document.getElementById('menu');
// 상위 요소(ul)에 이벤트 핸들러를 단 한 번만 등록
$menu.addEventListener('click', e => {
// 클릭된 요소가 li인 경우에만 처리
if (!e.target.matches('#menu > li')) return;
// 이벤트 처리 로직
console.log(`${e.target.id} 메뉴가 선택되었다.`);
});
이벤트 위임 사용 시 주의해야 할 점은 다음과 같다.
- 이벤트 타깃 검사: 상위 요소에 이벤트 핸들러를 등록하기 때문에, 실제로 이벤트를 발생시킨 요소(e.target)가 우리가 기대한 요소인지 반드시 확인해야 한다. Element.prototype.matches 메서드를 사용하여 이벤트 타깃이 원하는 선택자와 일치하는지 확인할 수 있다.
- 이벤트 전파 이해: 이벤트 위임은 이벤트 버블링을 활용하는 패턴이므로, 사용하려는 이벤트가 버블링되는 이벤트인지 확인해야 한다.
예를 들어, 더 복잡한 이벤트 위임의 예시를 살펴보자.
const $fruits = document.getElementById('fruits');
function activate({ target }) {
// 이벤트 타깃이 원하는 요소가 아닌 경우 무시
if (!target.matches('#fruits > li')) return;
// 모든 하위 요소에 대해 처리
[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
// 선택된 요소에만 'active' 클래스를 추가하고,
// 나머지 요소에서는 제거한다
});
}
// 상위 요소에만 이벤트 핸들러를 등록
$fruits.onclick = activate;
DOM 요소의 기본 동작 조작
DOM 요소의 기본 동작 중단
DOM 요소들은 각각 고유한 기본 동작을 가지고 있다. 예를 들어:
- a 태그는 클릭 시 href 속성에 지정된 URL로 페이지를 이동한다
- 폼 요소는 submit 시 페이지를 새로고침한다
- 체크박스는 클릭 시 체크/해제 상태가 변경된다
- 마우스 오른쪽 버튼 클릭 시 컨텍스트 메뉴가 표시된다
이러한 기본 동작이 웹 애플리케이션의 요구사항과 충돌할 때가 있다. 이벤트 객체의 preventDefault 메서드를 사용하면 이러한 기본 동작을 중단할 수 있다.
// a 태그 기본 동작 중단 예시
const $link = document.querySelector('a');
$link.addEventListener('click', e => {
e.preventDefault(); // 링크 이동을 중단한다
// 사용자 정의 동작을 실행한다
console.log('링크 클릭이 감지되었지만, 페이지 이동은 하지 않는다');
});
// 폼 제출 기본 동작 중단 예시
const $form = document.querySelector('form');
$form.addEventListener('submit', e => {
e.preventDefault(); // 폼 제출을 중단한다
// AJAX를 사용한 폼 데이터 제출 등 사용자 정의 동작을 수행할 수 있다
console.log('폼이 제출되었지만, 페이지 새로고침은 하지 않는다');
});
이벤트 전파 방지
이벤트 객체의 stopPropagation 메서드는 이벤트의 전파를 중단시킨다. 이는 특히 이벤트 위임을 사용하는 상황에서 특정 요소의 이벤트를 독립적으로 처리하고 싶을 때 유용하다.
// 상위 요소의 이벤트 핸들러
document.querySelector('.container').addEventListener('click', e => {
// 버튼이 클릭되면 배경색을 빨간색으로 변경
if (e.target.matches('button')) {
e.target.style.backgroundColor = 'red';
}
});
// 특정 버튼의 이벤트 핸들러
document.querySelector('.special-button').addEventListener('click', e => {
e.stopPropagation(); // 이벤트 전파를 중단한다
// 이 버튼은 상위 요소의 이벤트 핸들러에 영향을 받지 않고
// 독립적으로 파란색으로 변경된다
e.target.style.backgroundColor = 'blue';
});
이벤트 핸들러 내부의 this
이벤트 핸들러 어트리뷰트 방식
이벤트 핸들러 어트리뷰트 방식에서 this의 바인딩은 일반적인 함수 호출 규칙을 따른다. 이는 이벤트 핸들러가 일반 함수로 호출되기 때문에 this가 전역 객체(window)를 가리키게 된다.
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
console.log(this); // window
}
</script>
그러나 이벤트 핸들러를 호출할 때 인수로 this를 전달하면, 이는 이벤트를 바인딩한 DOM 요소를 가리키게 된다.
<button onclick="handleClick(this)">Click me</button>
<script>
function handleClick(element) {
console.log(element); // 버튼 요소
console.log(this); // window
}
</script>
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식
이 두 방식에서 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 이는 이벤트 객체의 currentTarget 프로퍼티와 같은 값이다.
const $button = document.querySelector('button');
// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function(e) {
console.log(this); // 버튼 요소
console.log(e.currentTarget); // 버튼 요소
console.log(this === e.currentTarget); // true
};
// addEventListener 메서드 방식
$button.addEventListener('click', function(e) {
console.log(this); // 버튼 요소
console.log(e.currentTarget); // 버튼 요소
console.log(this === e.currentTarget); // true
});
그러나 화살표 함수를 이벤트 핸들러로 사용하면 this 바인딩이 달라진다. 화살표 함수는 자신만의 this 바인딩을 생성하지 않고 상위 스코프의 this를 그대로 사용한다.
const $button = document.querySelector('button');
$button.addEventListener('click', e => {
console.log(this); // window
console.log(e.currentTarget); // 버튼 요소
console.log(this === e.currentTarget); // false
});
클래스 내부에서 이벤트 핸들러를 사용할 때는 특히 this 바인딩에 주의해야 한다.
class Counter {
constructor() {
this.$button = document.querySelector('.increase');
this.count = 0;
// 1. bind 메서드를 사용한 방법
this.$button.onclick = this.increase.bind(this);
// 2. 화살표 함수를 사용한 방법
this.$button.onclick = () => this.increase();
}
increase() {
this.$button.textContent = ++this.count;
}
}
new Counter();
클래스에서 이벤트 핸들러를 바인딩할 때는 다음 두 가지 방법을 주로 사용한다.
- bind 메서드를 사용하여 명시적으로 this를 바인딩
- 화살표 함수를 사용하여 렉시컬 스코프의 this를 유지
class Counter {
constructor() {
this.$button = document.querySelector('.increase');
this.count = 0;
this.$button.onclick = this.increase;
}
// 클래스 필드에 화살표 함수로 메서드를 정의
increase = () => {
this.$button.textContent = ++this.count;
};
}
또는 위처럼 클래스 필드에 화살표 함수를 할당하는 방법도 있다.
이벤트 핸들러에 인수 전달
이벤트 핸들러에 인수를 전달하는 방법은 이벤트 핸들러의 등록 방식에 따라 다르다. 이벤트 핸들러 어트리뷰트 방식의 경우, 함수 호출문을 사용할 수 있기 때문에 직접적으로 인수를 전달할 수 있다. 그러나 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식은 브라우저가 이벤트 핸들러를 호출하기 때문에 함수 자체를 등록해야 하며, 이로 인해 인수를 직접 전달하기는 어렵다.
하지만 이런 제약에도 불구하고, 다음과 같은 두 가지 방법으로 이벤트 핸들러에 인수를 전달할 수 있다.
이벤트 핸들러 내부에서 함수를 호출하는 방식
const MIN_USER_NAME_LENGTH = 5;
const $input = document.querySelector('input[type=text]');
const $msg = document.querySelector('.message');
// 실제 로직을 수행하는 함수
const checkUserNameLength = min => {
$msg.textContent = $input.value.length < min ?
`이름은 ${min}자 이상 입력해야 한다.` : '';
};
// 이벤트 핸들러 내부에서 함수를 호출하면서 인수 전달
$input.onblur = () => {
checkUserNameLength(MIN_USER_NAME_LENGTH);
};
이벤트 핸들러 내부에서 다른 함수를 호출하면서 필요한 인수를 전달할 수 있다.
이벤트 핸들러를 반환하는 함수를 사용하는 방식
const MIN_USER_NAME_LENGTH = 5;
const $input = document.querySelector('input[type=text]');
const $msg = document.querySelector('.message');
// 이벤트 핸들러를 반환하는 함수
const checkUserNameLength = min => e => {
$msg.textContent = $input.value.length < min ?
`이름은 ${min}자 이상 입력해야 한다.` : '';
};
// 함수를 호출하여 이벤트 핸들러를 반환받아 등록
$input.onblur = checkUserNameLength(MIN_USER_NAME_LENGTH);
함수가 이벤트 핸들러를 반환하도록 구현하면, 함수를 호출하면서 인수를 전달할 수 있다.
이 방식은 클로저를 활용하여 인수를 기억하는 새로운 함수를 반환한다. 반환된 함수는 이벤트 발생 시 기억된 인수를 사용할 수 있다.
커스텀 이벤트
커스텀 이벤트 생성
커스텀 이벤트는 개발자가 의도적으로 생성한 이벤트를 말한다. Event, UIEvent, MouseEvent 등의 이벤트 생성자 함수를 사용하여 명시적으로 이벤트 객체를 생성할 수 있다. 생성자 함수는 첫 번째 인수로 이벤트 타입을 나타내는 문자열을 받는다.
이벤트 타입은 기존의 이벤트 타입을 사용할 수도 있고, 새로운 이벤트 타입을 지정할 수도 있다.
// 기존 이벤트 타입을 사용하는 경우
const mouseEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: 50,
clientY: 100
});
// 새로운 이벤트 타입을 사용하는 경우
const customEvent = new CustomEvent('myCustomEvent', {
detail: { message: '커스텀 이벤트가 발생했다.' }
});
생성된 커스텀 이벤트 객체는 다음과 같은 특징을 갖는다.
- 버블링과 취소 가능 여부
- 기본적으로 버블링되지 않으며(bubbles: false), preventDefault 메서드로 취소할 수 없다(cancelable: false).
- 두 번째 인수로 옵션 객체를 전달하여 이러한 기본 동작을 변경할 수 있다.
- 이벤트 고유의 프로퍼티
- 이벤트 생성자 함수의 두 번째 인수로 이벤트 타입에 따른 고유 프로퍼티 값을 지정할 수 있다
// MouseEvent의 경우 마우스 좌표 정보 지정
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: 50,
clientY: 100,
button: 0 // 왼쪽 버튼 클릭
});
// KeyboardEvent의 경우 키 정보 지정
const keyboardEvent = new KeyboardEvent('keyup', {
key: 'Enter',
ctrlKey: false,
shiftKey: false
});
- CustomEvent 생성자 함수
- 커스텀 이벤트 객체를 생성할 때 주로 사용된다.
- detail 프로퍼티를 사용하여 이벤트와 함께 전달하고 싶은 데이터를 전달할 수 있다.
const customEvent = new CustomEvent('userAction', {
detail: {
id: 1,
username: 'Lee',
timestamp: Date.now()
}
});
- isTrusted 프로퍼티
- 이벤트가 사용자의 행위에 의해 발생한 것인지, 스크립트에 의해 인위적으로 생성된 것인지를 나타낸다.
- 커스텀 이벤트의 경우 항상 false다
console.log(customEvent.isTrusted); // false
커스텀 이벤트 디스패치
생성된 커스텀 이벤트는 dispatchEvent 메서드를 사용하여 발생시킬 수 있다. 이를 이벤트 디스패치라고 하며, 다음과 같은 특징을 갖는다.
- 동기적 처리
- 일반적인 이벤트 핸들러는 비동기적으로 동작하지만, dispatchEvent 메서드는 이벤트 핸들러를 동기적으로 호출한다.
- 이는 dispatchEvent 메서드를 호출하면 커스텀 이벤트에 바인딩된 이벤트 핸들러가 즉시 호출됨을 의미한다.
const $button = document.querySelector('.btn');
// 이벤트 핸들러 등록 - 반드시 디스패치 이전에 등록해야 한다
$button.addEventListener('customClick', e => {
console.log(e.detail.message); // "안녕하세요!"
console.log('커스텀 이벤트 실행');
});
// 커스텀 이벤트 생성
const customEvent = new CustomEvent('customClick', {
detail: { message: '안녕하세요!' }
});
// 커스텀 이벤트 디스패치
$button.dispatchEvent(customEvent);
console.log('디스패치 완료'); // 이벤트 핸들러 실행 후 출력된다
- 이벤트 핸들러 등록 방식의 제약
- 커스텀 이벤트는 반드시 addEventListener 메서드를 사용하여 이벤트 핸들러를 등록해야 한다.
- 이벤트 핸들러 어트리뷰트나 프로퍼티 방식은 사용할 수 없다.
// 이벤트 핸들러 프로퍼티 방식 - 동작하지 않는다
$button.oncustomClick = e => {
console.log(e.detail.message); // 실행되지 않는다
};
// addEventListener 방식 - 정상 동작한다
$button.addEventListener('customClick', e => {
console.log(e.detail.message); // 정상적으로 실행된다
});
실제 활용 예제를 살펴보자.
// 폼 유효성 검사 후 커스텀 이벤트 발생시키기
const $form = document.querySelector('form');
const $input = document.querySelector('input');
// 폼 검증 성공/실패 이벤트 핸들러 등록
$form.addEventListener('validationSuccess', e => {
console.log('유효성 검사 성공:', e.detail.value);
// 폼 제출 처리
});
$form.addEventListener('validationError', e => {
console.log('유효성 검사 실패:', e.detail.error);
// 에러 메시지 표시
});
// 입력값 검증 후 적절한 커스텀 이벤트 발생
$input.addEventListener('input', e => {
const value = e.target.value;
if (value.length >= 5) {
const successEvent = new CustomEvent('validationSuccess', {
detail: { value }
});
$form.dispatchEvent(successEvent);
} else {
const errorEvent = new CustomEvent('validationError', {
detail: { error: '최소 5글자 이상 입력해야 한다.' }
});
$form.dispatchEvent(errorEvent);
}
});
이러한 커스텀 이벤트는 애플리케이션의 다양한 부분 간의 통신을 구현하거나, 복잡한 상태 변화를 처리하는 데 유용하게 사용될 수 있다.
요약
이벤트 드리븐 프로그래밍의 기본 개념
- 브라우저는 사용자의 행동(클릭, 키보드 입력 등)을 감지하여 이벤트를 발생시킨다.
- 이벤트 핸들러는 이벤트 발생 시 호출되는 함수이다.
- 이벤트 핸들러 등록은 브라우저에게 이벤트 발생 시 실행할 함수를 알려주는 과정이다.
- 이벤트 기반 프로그래밍은 현대 웹 애플리케이션의 핵심 패러다임이다.
이벤트 타입
- 약 200여 가지의 이벤트 타입이 존재한다.
- 주요 이벤트 카테고리:
- 마우스 이벤트: click, dblclick, mousedown, mouseup, mousemove 등
- 키보드 이벤트: keydown, keyup (keypress는 deprecated)
- 포커스 이벤트: focus/blur(버블링X), focusin/focusout(버블링O)
- 폼 이벤트: submit, reset
- 값 변경 이벤트: input, change, readystatechange
- DOM 뮤테이션 이벤트: DOMContentLoaded
- 뷰 이벤트: resize, scroll
- 리소스 이벤트: load, unload, abort, error
이벤트 핸들러 등록 방식
- 이벤트 핸들러 어트리뷰트 방식
- HTML 요소에 직접 on + 이벤트타입 형태로 등록
- 현대에는 권장되지 않지만, 프레임워크(React 등)에서는 유사한 방식 사용
- 이벤트 핸들러 프로퍼티 방식
- DOM 요소의 프로퍼티로 이벤트 핸들러 등록
- 한 이벤트당 하나의 핸들러만 등록 가능
- addEventListener 메서드 방식
- 가장 권장되는 방식
- 여러 이벤트 핸들러 등록 가능
- 캡처링/버블링 단계 선택 가능
이벤트 객체
- 이벤트 발생 시 자동으로 생성되는 객체
- 이벤트의 상세 정보를 담고 있다
- 주요 공통 프로퍼티:
- type: 이벤트 종류
- target: 실제 이벤트가 발생한 요소
- currentTarget: 이벤트 핸들러가 바인딩된 요소
- eventPhase: 이벤트 전파 단계
- bubbles: 버블링 가능 여부
- cancelable: 기본 동작 취소 가능 여부
이벤트 전파
- 캡처링 단계: window에서 타깃 요소로 내려가는 단계
- 타깃 단계: 실제 이벤트가 발생한 요소에 도달한 단계
- 버블링 단계: 타깃에서 window로 올라가는 단계
- 일부 이벤트(focus/blur 등)는 버블링되지 않는다
이벤트 위임
- 여러 하위 요소의 이벤트를 상위 요소에서 처리하는 패턴
- 장점:
- 메모리 사용량 감소
- 동적 요소 처리 용이
- 이벤트 핸들러 관리 단순화
- Element.prototype.matches로 타깃 요소 검증 필요
DOM 요소의 기본 동작 제어
- preventDefault(): 요소의 기본 동작 중단
- stopPropagation(): 이벤트 전파 중단
이벤트 핸들러 내부의 this
- 어트리뷰트 방식: 전역 객체(window) 참조
- 프로퍼티/addEventListener 방식: 이벤트가 바인딩된 요소 참조
- 화살표 함수 사용 시: 상위 스코프의 this 참조
커스텀 이벤트
- Event 생성자나 CustomEvent 생성자로 생성
- dispatchEvent 메서드로 이벤트 발생시킴
- detail 프로퍼티로 추가 정보 전달 가능
- addEventListener로만 이벤트 핸들러 등록 가능
주의사항 및 최적화
- 불필요한 이벤트 핸들러 제거
- 이벤트 위임 패턴 적극 활용
- 리소스 집약적인 이벤트(scroll, resize)는 디바운싱/쓰로틀링 적용
- 이벤트 핸들러 내부의 this 바인딩 주의
- 커스텀 이벤트 사용 시 동기/비동기 동작 이해 필요
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
마우스 이벤트 타입에는 뭐가 있나요? click 말고 클릭을 대체할 수 있는 이벤트가 있나요?
마우스 이벤트에는 여러 가지 타입이 있습니다. 가장 기본적인 click 이벤트 외에도, mousedown과 mouseup을 통해 클릭을 대체할 수 있습니다.
mousedown은 마우스 버튼을 누르는 순간, mouseup은 마우스 버튼을 놓는 순간 발생하는 이벤트입니다. 사실 click 이벤트는 mousedown과 mouseup이 순차적으로 발생한 후에 발생하는 이벤트입니다.
이외에도 dblclick(더블클릭), mousemove(마우스 이동), mouseenter/mouseleave(요소 진입/이탈, 버블링 없음), mouseover/mouseout(요소 진입/이탈, 버블링 있음) 등이 있습니다.
특히 mouseenter/mouseleave와 mouseover/mouseout의 차이점은 버블링 여부입니다. mouseenter와 mouseleave는 버블링이 발생하지 않아 정확한 요소의 진입과 이탈을 감지할 수 있고, mouseover와 mouseout은 버블링이 발생하여 부모 요소에도 이벤트가 전파됩니다.
그 외에 알고 있는 대표적인 이벤트가 있나요?
키보드, 폼, 포커스 관련 이벤트 등 여러 종류가 있습니다.
키보드 이벤트로는 keydown(키를 누를 때), keyup(키를 놓을 때)이 있습니다. keypress도 있었지만 현재는 deprecated되었습니다.
폼 관련 이벤트로는 submit(폼 제출), reset(폼 초기화), input(값 입력 시 실시간 발생), change(값 변경이 완료될 때) 등이 있습니다.
포커스 관련 이벤트로는 focus/blur(포커스 획득/상실, 버블링 없음)와 focusin/focusout(포커스 획득/상실, 버블링 있음)이 있습니다.
문서 로딩 관련 이벤트로는 DOMContentLoaded(HTML 파싱 완료), load(모든 리소스 로딩 완료), unload(페이지 이탈), error(리소스 로딩 실패) 등이 있습니다.
또한 scroll(스크롤), resize(창 크기 변경) 같은 뷰 이벤트도 있습니다.
이벤트 핸들러를 등록하는 방식에는 어떤 것들이 있나요?
이벤트 핸들러를 등록하는 방식은 크게 세 가지가 있습니다.
첫 번째는 이벤트 핸들러 어트리뷰트 방식입니다.
<button onclick="handleClick()">Click me</button>
HTML 요소의 속성으로 직접 이벤트 핸들러를 등록하는 방식입니다.
이 방식은 HTML과 자바스크립트가 혼재되어 관심사 분리 측면에서 좋지 않아 현대에는 권장되지 않습니다. 하지만 React와 같은 컴포넌트 기반 프레임워크에서는 이와 유사한 방식을 사용합니다.
두 번째는 이벤트 핸들러 프로퍼티 방식입니다.
element.onclick = function() {
// 이벤트 처리
};
이 방식은 HTML과 자바스크립트가 분리되어 있다는 장점이 있지만, 하나의 이벤트에 하나의 핸들러만 바인딩할 수 있다는 단점이 있습니다.
세 번째는 addEventListener 메서드 방식입니다.
element.addEventListener('click', function() {
// 이벤트 처리
});
이 방식이 가장 권장되는 방식입니다. 하나의 이벤트에 여러 개의 핸들러를 등록할 수 있고, 캡처링과 버블링 단계를 선택할 수 있으며, removeEventListener를 통해 핸들러를 제거할 수도 있습니다.
이벤트 전파(propagation)에 대해서 알고 있나요?
이벤트 전파는 DOM 트리에서 이벤트가 전달되는 방식을 말하며, 크게 세 단계로 이루어집니다.
첫 번째는 캡처링 단계입니다. 이벤트가 최상위 조상 요소인 window로부터 시작하여 이벤트가 발생한 타깃 요소까지 아래 방향으로 전파됩니다.
두 번째는 타깃 단계입니다. 이벤트가 실제로 발생한 타깃 요소에 도달한 단계입니다.
세 번째는 버블링 단계입니다. 이벤트가 타깃 요소로부터 다시 window까지 상위 방향으로 전파됩니다.
예를 들어, 다음과 같은 구조가 있다고 가정해보겠습니다.
<div>
<ul>
<li>텍스트</li>
</ul>
</div>
li 요소를 클릭하면, 이벤트는 window → document → html → body → div → ul → li (캡처링 단계), 그다음 li(타깃 단계), 마지막으로 li → ul → div → body → html → document → window (버블링 단계) 순으로 전파됩니다.
단, 모든 이벤트가 버블링되는 것은 아닙니다. focus, blur, load, unload, mouseenter, mouseleave 등의 이벤트는 버블링되지 않습니다. 이런 이벤트들은 상위 요소에서 캐치하려면 캡처링을 사용하거나, 버블링이 되는 유사 이벤트(focusin/focusout, mouseover/mouseout)를 사용해야 합니다.
이벤트 위임(delegation)에 대해서 알고있나요? 🔥
이벤트 위임은 여러 하위 요소들에 각각 이벤트 핸들러를 등록하는 대신, 공통 조상 요소에 이벤트 핸들러를 등록하여 이벤트를 관리하는 방식입니다.
이는 이벤트 버블링을 활용한 패턴으로, 다음과 같은 장점들이 있습니다.
첫째, 메모리 사용량이 줄어듭니다. 예를 들어 1000개의 li 요소에 각각 이벤트 핸들러를 등록하는 대신, 상위의 ul 요소 하나에만 이벤트 핸들러를 등록하면 되므로 메모리 효율이 높아집니다.
둘째, 동적으로 추가되는 요소들에 대해서도 별도의 이벤트 핸들러 등록이 필요 없습니다. 예를 들어보겠습니다.
const ul = document.querySelector('ul');
ul.addEventListener('click', e => {
// 이벤트가 발생한 요소가 li인 경우에만 처리
if (e.target.matches('li')) {
// 이벤트 처리 로직
console.log(e.target.textContent);
}
});
이후에 li 요소가 동적으로 추가되어도 자동으로 이벤트 처리가 가능합니다.
셋째, 코드가 깔끔해지고 관리가 용이해집니다. 이벤트 핸들러가 한 곳에서 집중적으로 관리되기 때문입니다.
다만 주의할 점은, 이벤트가 실제로 발생한 요소(e.target)가 우리가 원하는 요소인지 반드시 확인해야 합니다. 이를 위해 Element.prototype.matches 메서드를 사용하여 태그나 클래스를 체크합니다.
e.preventDefault 에 대해 알고 있나요?
preventDefault는 이벤트의 기본 동작을 중단시키는 메서드입니다. DOM 요소들은 각각 기본적인 동작을 가지고 있는데, 이를 막고 싶을 때 사용합니다. 대표적인 예시를 몇 가지 들어보겠습니다.
첫째, 폼 제출의 경우입니다.
form.addEventListener('submit', e => {
e.preventDefault(); // 폼 제출로 인한 페이지 새로고침 방지
// AJAX를 이용한 폼 데이터 전송 등의 커스텀 로직
});
둘째, a 태그의 링크 이동을 막는 경우입니다.
link.addEventListener('click', e => {
e.preventDefault(); // 링크로의 이동을 막음
// 다른 처리 로직
});
셋째, 마우스 오른쪽 버튼 클릭 시 컨텍스트 메뉴를 막는 경우입니다.
document.addEventListener('contextmenu', e => {
e.preventDefault(); // 기본 컨텍스트 메뉴 표시 방지
// 커스텀 메뉴 표시 등의 로직
});
단, cancelable 속성이 true인 이벤트만 preventDefault로 취소할 수 있습니다. 모든 이벤트의 기본 동작을 취소할 수 있는 것은 아닙니다.
e.stopPropagation
stopPropagation은 이벤트의 전파(버블링과 캡처링)를 중단시키는 메서드입니다. 이벤트가 부모나 자식 요소로 전파되는 것을 막고 싶을 때 사용합니다.
예를 들어, 모달 내부의 버튼 클릭 시 모달은 닫히지 않고 버튼의 이벤트만 처리하고 싶을 때 사용할 수 있습니다.
modalBackground.addEventListener('click', () => {
// 모달 닫기
modal.style.display = 'none';
});
modalButton.addEventListener('click', e => {
e.stopPropagation(); // 이벤트가 모달 배경까지 전파되는 것을 방지
// 버튼 클릭 처리
});
또는 특정 요소의 이벤트를 독립적으로 처리하고 싶을 때도 사용합니다.
parent.addEventListener('click', () => {
console.log('부모 요소 클릭');
});
child.addEventListener('click', e => {
e.stopPropagation(); // 부모로의 이벤트 전파 중단
console.log('자식 요소 클릭');
});
stopPropagation과 preventDefault를 혼동하는 경우가 있는데, stopPropagation은 이벤트 전파를 중단하는 것이고, preventDefault는 요소의 기본 동작을 중단하는 것이라는 점에서 차이가 있습니다. 두 메서드는 독립적으로 동작하며, 필요에 따라 둘 다 사용할 수도 있습니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 42장 - 비동기 프로그래밍 (0) | 2024.11.12 |
---|---|
[모던 자바스크립트 Deep Dive] 41장 - 타이머 (0) | 2024.11.12 |
[모던 자바스크립트 Deep Dive] 39장 - DOM (0) | 2024.11.11 |
[모던 자바스크립트 Deep Dive] 38장 - 브라우저의 렌더링 과정 (3) | 2024.10.31 |
[모던 자바스크립트 Deep Dive] 36장 - 배열 디스트럭처링 할당 (0) | 2024.10.25 |