Index Index
- ① 이벤트 드리븐 프로그래밍
- ② 이벤트 핸들러 등록 방식
- ③ 이벤트 전파 (Event Propagation)
- ④ 이벤트 객체
- ⑤ 고급 이벤트 처리 기법
- ⑥ 이벤트 핸들러 내부의 this
- ⑦ 이벤트 핸들러에 인수 전달
- ⑧ 커스텀 이벤트
🧐 Q. 브라우저가 핸들러를 호출한다는게 무엇일까?
Dom(Document Object Model)은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API이다.
1. 이벤트 드리븐 프로그래밍
1.1 핵심 원리
- 이벤트 발생 → 브라우저가 핸들러 호출 → 앱의 흐름이 결정됨.
- 이벤트 타입은 약 200가지
1.2 주요 이벤트 유형
- 마우스: click, mouseover, mouseenter 등
- 키보드: keydown, keyup
- 포커스: focus, blur
- 폼: submit, reset
- 변경: input, change
- DOM 로딩: DOMContentLoaded
- 뷰: scroll, resize
- 리소스: load, error 등
2. 이벤트 핸들러 등록 방식
2.1 어트리뷰트 방식
<button onclick = "sayHi()">- HTML/JS 혼합 → 유지보수 어려움
2.2 프로퍼티 방식
element.onclick = fn- 장점: JS와 HTML 분리
- 단점: 1개 핸들러만 바인딩 가능
2.3 addEventListener 방식
-
element.addEventListener(‘click’, fn, useCapture)
-
장점
-
여러 핸들러 등록 가능
<!DOCTYPE html> <html> <body> <button>Click me!</button> <script> // 버튼 요소 선택 const $button = document.querySelector("button"); // addEventListener는 동일 요소에서 동일 이벤트에 대해 // 여러 개의 이벤트 핸들러를 등록할 수 있다. $button.addEventListener("click", function () { console.log("[1] button click"); }); $button.addEventListener("click", function () { console.log("[2] button click"); }); </script> </body> </html> -
캡처링 제어
-
제거 가능(
removeEventListener)
-
-
실무/표준에서 가장 많이 사용
3. 이벤트 전파(Event Propagation)
3.1 전파 단계
- Capturing: window → target까지 내려옴
- Target: 실제 이벤트 발생
- Bubbling: target → window로 올라감
3.2 특징
- 기본적으로 버블링 단계에서 핸들러 동작
- 캡처링 사용하려면 등록시 옵션
true - 버블링되지 않는 이벤트: focus/blur, mouseenter/leave, load/error
4. 이벤트 객체
4.1 공통 프로퍼티
type: 이벤트 타입target: 이벤트 발생 요소currentTarget: 핸들러가 바인딩된 요소eventPhase: 전파 단계bubbles,cancelable,isTrusted,timeStamp등
4.2 세부 정보
MouseEvent: 좌표(clientX/Y), 버튼KeyboardEvent: key, code, ctrlKey 등
5. 고급 이벤트 처리 기법
5.1 이벤트 위임(Event Delegation)
- 상위 요소에 하나의 핸들러만 등록해 하위 요소 이벤트를 처리
- 원리: 버블링
- 예시
<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}
#fruits li {
width: 100px;
cursor: pointer;
}
#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
<body>
<nav>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</nav>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById("fruits");
const $msg = document.querySelector(".msg");
// 사용자 클릭에 의해 선택된 li에 active 클래스를 추가하고,
// 나머지 li에서는 active 클래스를 제거한다.
function activate({ target }) {
// 이벤트를 발생시킨 요소가 #fruits의 자식 li가 아니면 무시
if (!target.matches("#fruits li")) return;
// 모든 li에 대해 active 클래스를 토글
[...$fruits.children].forEach(($fruit) => {
$fruit.classList.toggle("active", $fruit === target);
});
// 선택된 과일의 id를 메시지에 표시
$msg.textContent = target.id;
}
// 이벤트 위임: 상위 요소(ul#fruits)가 하위 li 요소의 클릭 이벤트를 한 번에 처리
$fruits.onclick = activate;
</script>
</body>
</html>
- 장점
- 성능 최적화
- 동적 요소 관리 용이
- 코드 간소화
5.2 기본 동작 & 전파 제어
- 기본동작 차단:
preventDefault()
DOM 요소마다 기본 행동(브라우저가 자동 수행하는 동작)이 있는데 '’그 기본 동작을 하지말라’‘고 브라우저에게 명령하는 것
<form> → submit 발생
<checkbox> 클릭 → 체크 상태 변경
<input type="text"> 에서 Enter → 폼 제출
<contextmenu> → 우클릭 메뉴 표시
-
전파 중단:
stopPropagation()“이벤트가 더 이상 부모로 올라가지 못하게 해라.”라고 브라우저에게 명령하는 것
document.querySelector(".btn2").onclick = (e) => {
e.stopPropagation();
e.target.style.color = "blue";
};
/*
btn2는 자기 자신의 클릭 이벤트만 실행
부모 요소 .container에는 이벤트가 전달되지 않음
→ 그래서 부모의 이벤트 위임이 btn2에는 적용되지 않음
*/
6. 이벤트 핸들러 내부의 this
JavaScript에서 이벤트 핸들러 내부의 this는 이벤트를 등록하는 방식에 따라 달라진다. 특히 HTML 어트리뷰트 방식, 프로퍼티 방식, 그리고 addEventListener 방식은 this의 바인딩 방식이 서로 다르기 때문에 이를 명확하게 이해해야 예측 가능한 코드 작성이 가능하다.
6.1 이벤트 핸들러 어트리뷰트 방식
<button onclick="handleClick()">Click me</button>
<script>
function handleClick(button) {
console.log(button); // 클릭한 버튼 요소
console.log(this); // window
}
</script>
어트리뷰트 방식의 핵심 특징
- HTML 어트리뷰트로 지정된 이벤트 핸들러 코드는 실제로는 암묵적으로 전역 컨텍스트(window)에서 실행된다.
- 따라서 이벤트 핸들러 내부에서
this는 항상 window를 가리킨다.
6.2 이벤트 핸들러 프로퍼티 방식
다음은 DOM 요소 객체의 이벤트 프로퍼티(onclick, onchange 등)에 직접 핸들러를 할당하는 방식이다.
<button class="btn1">0</button>
<script>
const $button1 = document.querySelector(".btn1");
$button1.onclick = function (e) {
console.log(this); // $button1 (이벤트 바인딩된 DOM 요소)
console.log(e.currentTarget); // $button1
console.log(this === e.currentTarget); // true
};
</script>
프로퍼티 방식의 핵심 특징
- 이벤트 핸들러를 DOM 요소의 프로퍼티에 할당하면, 자바스크립트 엔진이 이를 메서드처럼 호출한다.
- 따라서 이벤트 핸들러 내부의
this는 이벤트가 바인딩된 해당 DOM 요소를 가리킨다. - 이는
e.currentTarget과 동일하다.
6.3 addEventListener 메서드 방식
addEventListener 방식은 자바스크립트에서 가장 권장되는 최신 이벤트 등록 방식이다.
<button class="btn2">0</button>
<script>
const $button2 = document.querySelector(".btn2");
$button2.addEventListener("click", function (e) {
console.log(this); // $button2
console.log(e.currentTarget); // $button2
console.log(this === e.currentTarget); // true
});
</script>
addEventListener 방식의 핵심 특징
- 프로퍼티 방식과 동일하게, 이벤트 핸들러 내부의
this는 이벤트가 바인딩된 DOM 요소이다. - 이벤트 객체의
currentTarget과 동일하다. - 여러 개의 핸들러를 하나의 이벤트에 중복 등록할 수 있다(프로퍼티 방식은 1개만 가능).
6.4 이벤트 핸들러 내부의 this
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Event Handler this 문제</title>
</head>
<body>
<button class="btn">0</button>
<script>
class App {
constructor() {
this.$button = document.querySelector(".btn");
this.count = 0;
// increase 메서드를 이벤트 핸들러로 등록
this.$button.onclick = this.increase;
}
increase() {
// 이벤트 핸들러 increase 내부의 this는
// App 인스턴스가 아닌 DOM 요소(button)를 가리킨다.
// 따라서 this.$button은 undefined
this.$button.textContent = ++this.count;
// ❌ TypeError: Cannot set property 'textContent' of undefined
}
}
new App();
</script>
</body>
</html>
🧐 혼동했던 부분
increase가 호출될 때의 this가 App 인스턴스가 아니라 버튼 DOM 요소라서, 그 버튼 객체에는 $button이라는 프로퍼티가 존재하지 않기 때문에 this.$button이 undefined가 되고, undefined.textContent를 읽으려다가 TypeError가 나는 것 입니다.
7. 이벤트 핸들러에 인수 전달
이벤트 핸들러는 개발자가 호출하는 함수가 아니라 브라우저가 제어·호출하는 함수이기 때문에 인수 전달이 제한되며, 이를 해결하기 위해 클로저나 wrapper 패턴이 필요하다는 점
1번은 “이벤트가 발생하면 함수를 호출하도록 위임하는 것”이고, 2번은 “이벤트에 대응할 함수를 미리 만들어서 넘기는 것”이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Wrapper 방식</title>
</head>
<body>
<label>
User name
<input type="text" />
</label>
<em class="message"></em>
<script>
// 1번
const MIN_USER_NAME_LENGTH = 5;
const $input = document.querySelector('input[type="text"]');
const $msg = document.querySelector(".message");
// 실제 로직 함수 (인수를 받아 실행됨)
function checkUserNameLength(min) {
$msg.textContent =
$input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : "";
}
// ✅ wrapper 함수가 이벤트 핸들러
$input.onblur = () => {
checkUserNameLength(MIN_USER_NAME_LENGTH);
};
</script>
<script>
// 2번
const MIN_USER_NAME_LENGTH = 5;
const $input = document.querySelector('input[type="text"]');
const $msg = document.querySelector(".message");
// ✅ 이벤트 핸들러를 반환하는 함수
const checkUserNameLength = (min) => (event) => {
$msg.textContent =
$input.value.length < min ? `이름은 ${min}자 이상 입력해 주세요` : "";
};
// ✅ 반환된 함수가 바로 이벤트 핸들러
$input.onblur = checkUserNameLength(MIN_USER_NAME_LENGTH);
</script>
</body>
</html>
Event Handler Argument Passing Patterns
JayTak
8. 커스텀 이벤트
8.1 커스텀 이벤트란?
브라우저가 자동으로 발생시키지 않는 “의미 있는 상태 변화나 의도”를 이벤트라는 공용 인터페이스로 표준화해 전달하기 위해서다.
🧐 왜 기본 이벤트(click, input 등)만으로는 부족한가?
기본 이벤트는 전부 사용자 행위 중심입니다.
- click, keydown, input → “무슨 행동을 했는가”
- 하지만 실제 앱에서는 더 많이 묻습니다
- “로그인이 완료되었는가?”
- “결제가 성공했는가?”
- “데이터 로딩이 끝났는가?”
- “검증이 통과되었는가?”
👉 이런 것들은 DOM에 기본 이벤트가 존재하지 않습니다.
그래서 커스텀 이벤트는 이렇게 쓰입니다:
“내 애플리케이션 내부에서 의미 있는 순간을 브라우저 이벤트 모델 위에 얹어 표현하는 수단”
🧐 dispatchEvent가 “직접 호출과 같다”는 말의 의미
책에서 말한 이 부분이 아주 중요합니다.
dispatchEvent는 이벤트 핸들러를 동기적으로 직접 호출하는 것과 같다
이 말의 진짜 뜻은:
- “가짜 이벤트”를 흉내내는 게 아니라
- 이벤트 흐름(caputre → target → bubble)을 그대로 타게 하면서
- 표준 이벤트 규약(
event,this, 전파 등)을 그대로 재사용
한다는 뜻입니다.
// 1. 일반 이벤트 (비동기처럼 동작)
button.onclick = () => console.log("clicked");
console.log("A");
// 사용자가 클릭
console.log("B");
// 2. dispatchEvent (동기 실행)
button.addEventListener("click", () => console.log("clicked"));
console.log("A");
button.dispatchEvent(new MouseEvent("click"));
console.log("B");
// 출력결과
A;
clicked;
B;
8.2 커스텀 이벤트의 제약
커스텀 이벤트는 on이벤트 방식으로는 받을 수 없고, 반드시 addEventListener로만 등록해야 한다.
임의로 만든 이벤트 타입(foo)에는 onfoo라는 속성이 DOM에 존재하지 않기 때문에, 이벤트 핸들러를 addEventListener로만 등록할 수 있다.
🧐 왜 onfoo는 사용할 수 없나?
1. 이벤트 핸들러 프로퍼티 방식의 정체
이 방식은 아무 이벤트에나 자동으로 동작하는 것이 아닙니다.
element.onclick = fn;
element.onblur = fn;
이건 단순한 규칙이 아니라,
✅ DOM 요소에 미리 정의되어 있는 프로퍼티입니다. 즉 이런 것들은 HTML / DOM 스펙에 하드코딩되어 있습니다.
button.onclick // ✅ 존재
button.onmouseover // ✅ 존재
button.onfoo // ❌ 정의된 적 없음
2. 커스텀 이벤트는 “이름만 정한 이벤트”
new CustomEvent('foo')
'foo'는 오직 이벤트 타입 문자열- DOM 스펙에 정의된 이벤트 아님
- 그래서 DOM에는:
button.onfoo === undefined
👉 존재하지 않는 프로퍼티에 핸들러를 넣을 수 없음
3. addEventListener는 왜 가능한가?
button.addEventListener('foo', handler);
addEventListener는 다릅니다.
- 이벤트 타입을 문자열로 받음
- DOM에 미리 정의돼 있든 없든 상관 없음
- “이 타입의 이벤트가 오면 이 핸들러 호출”만 등록