개요
이벤트는 프론트엔드 프로그래밍의 핵심이다.
때문에 프론트엔드에서 이벤트가 어떻게 처리되는지 흐름을 정리해 보았다.
내용을 이해하기 위해 사전에 이벤트와 이벤트 핸들러가 무엇인지 알아야 한다.
✅ 이벤트(Event)
- 사용자가 어떤 행동을 했을 때 발생하는 사건.
- 예) 클릭(click), 키 누름(keydown), 마우스 움직임(mousemove), 입력(input) 등
✅ 이벤트 핸들러(Event Handler)
- 이벤트가 발생했을 때 실행되는 함수.
- 예) 버튼을 클릭하면 어떤 코드를 실행하고 싶을 때, 그게 핸들러이다.
Event interface
우리가 웹에서 실행하는 이벤트는 자바스크립트의 Event interface에 맞춰서 생성된 객체이다.
자바스크립트에서 이벤트는 다음 코드로 정의된다.
interface Event {
constructor(DOMString type, optional EventInit eventInitDict = {});
readonly attribute DOMString type;
readonly attribute EventTarget? target;
readonly attribute EventTarget? srcElement; // legacy
readonly attribute EventTarget? currentTarget;
sequence<EventTarget> composedPath();
const unsigned short NONE = 0;
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;
readonly attribute unsigned short eventPhase;
undefined stopPropagation();
attribute boolean cancelBubble; // legacy alias of .stopPropagation()
undefined stopImmediatePropagation();
readonly attribute boolean bubbles;
readonly attribute boolean cancelable;
attribute boolean returnValue; // legacy
undefined preventDefault();
readonly attribute boolean defaultPrevented;
readonly attribute boolean composed;
[LegacyUnforgeable] readonly attribute boolean isTrusted;
readonly attribute DOMHighResTimeStamp timeStamp;
undefined initEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false); // legacy
};
dictionary EventInit {
boolean bubbles = false;
boolean cancelable = false;
boolean composed = false;
};
keydown , submit , click 과 같은 이벤트가 웹에서 발생하면 브라우저는 위와 같은 Event 객체를 만들어서 핸들러에 넘겨준다.
Event flow
벤트는 캡쳐링 단계 ⇒ 타겟 단계 ⇒ 버블링 단계 의 순서로 위에서 아래 → 아래에서 위로 전달된다.
각 단계는 다음과 같이 정의된다.
1. Capture phase
이벤트 객체가 Window부터 이벤트가 등록된 요소(element)까지 전달되는 단계이다.
이때 전파 경로의 시작은 Window가 되며 마지막은 이벤트가 등록된 요소가 된다.
2. Target phase
이벤트 객체가 event target에 도달한 단계를 말한다.
만약에 Event type이 다음 phase를 지원하지 않는 경우, 다음 단계는 넘어가고 전파가 종료된다.
3. Bubble phase
이벤트 객체가 capture 단계에서 전달되었던 순서의 반대로 진행되는 단계이다.
부모 엘리먼트로 전달이 진행되면서 최종적으로 Window까지 이벤트가 전달된다. 이 단계에서는 Window가 전파 경로의 마지막이 되는 것이다.
이벤트는 위 세 단계로 전파되며 세 단계 중 어느 단계에서 이벤트 핸들러를 발생(trigger)시킬 것인가에 따라 그 목적이 달라질 수 있다.
Not support phase
이벤트에 따라 지원하는 phase가 있고, 지원하지 않는 phase가 존재한다.
그래서 위 단계별로 이벤트가 전달될 때, 지원되지 않는 phase는 건너뛰게 된다.
그 외 전파되는 이벤트 객체는 개발자가 강제로 멈추지 않는 이상 결정된 전파 경로를 따라 전파된다.
예를 들면 타입이 focus인 Event는 bubbling 되지 않는다.
여러 DOM Element로 구성된 하나의 웹 페이지는 Window를 최상위로 하는 트리를 생성하게 된다.
이벤트는 이벤트 각각이 갖게 되는 전파 경로(propagation path)를 따라 전파된다.
그리고 이 전파 경로는 DOM Tree 구조에서 Element의 위상(hierarchy)에 의해 결정이 된다.
<html>
<body>
<div id="outer">
<button id="btn">클릭!</button>
</div>
</body>
</html>
예를 들어 위와 같은 코드가 실행된다면 이벤트는 다음과 같은 흐름으로 실행된다.
window → document → html → body → div → button (⬇️ 캡처링)
button (🎯 실제 클릭한 타겟) → div → body → html → document → window (⬆️ 버블링)
클릭 이벤트를 수행한 button 객체는 캡쳐링 단계를 거쳐 이벤트를 전달한다.
그 후 클릭 이벤트가 수행된 button 객체를 찾게 되면 타겟팅 단계가 수행된다.
event 객체에서 접근할 수 있는 target이 이 값이다.
이후 이벤트 객체는 다시 부모 요소로 전달이 진행되고, 최종적으로 window까지 이벤트가 전달됨으로써 이벤트가 수행된다. 이를 버블링 단계라고 한다.
Bubbling Event
버블링으로 이벤트를 등록한다는 것은 무슨 의미인가?
Add Event Listener
addEventListener은 다음과 같은 코드로 구성되어 있다.
interface EventTarget {
constructor();
undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {});
undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {});
boolean dispatchEvent(Event event);
};
callback interface EventListener {
undefined handleEvent(Event event);
};
dictionary EventListenerOptions {
boolean capture = false;
};
dictionary AddEventListenerOptions : EventListenerOptions {
boolean passive;
boolean once = false;
AbortSignal signal;
};
위 코드에서 addEventListener 함수는 세 번째 인자로 options를 받는 것을 알 수 있다.
options에서 capture 속성은 이벤트가 어느 단계에서 수행될 지 정한다.
위 코드처럼 기본적으로 false를 가지며 Bubbling 단계에서 이벤트를 수행하겠다는 의미이다.
만약 값을 true로 준다면 Capturing 단계에서 이벤트가 수행된다.
Trusted Event
사용자에 의해 발생한 이벤트인지 브라우저(user agent)에 의해 발생한 이벤트인지 Event 객체의 속성을 통해 판단할 수 있다.

click() 함수를 통해 수행된 이벤트의 isTrusted 값은 false이다.

사용자가 요소를 직접 클릭하여 수행된 이벤트의 isTrusted 값은 true이다.
Stop propagation
이벤트의 전파를 막을 수도 있다.
예제 코드로 살펴보자
1. 버블링 막기
<div id="parent" onclick="console.log('Hello, i want to logging!')">
<button id="bubbling-stop-button-1">Stop</button>
</div>
<script>
$('#bubbling-stop-button-1').on('click', e => {
e.stopPropagation()
console.log("I'm bubbling-stop-button-1")
})
</script>
위 코드에서 div 태그의 이벤트는 수행되지 않는다.
어느 phase에 핸들러를 실행시킬지 명시하지 않았기 때문에 div#parent 엘리먼트의 이벤트 핸들러는 Bubbling Phase에 실행된다.
그러나 div#parent 엘리먼트의 자식인 button#bubbling-stop-button-1 엘리먼트에서 이벤트 핸들러가 실행된 후, 전파를 중단했기 때문에 상위 엘리먼트에서는 click 이벤트를 전파받을 수 없다.
때문에 콘솔에는 “I’m bubbling-stop-button-1”만 출력된다.
2. 캡쳐링 단계에서 이벤트 먼저 수행하기
<div id="capture">
<button id="bubbling-stop-button-2">Stop</button>
</div>
<script>
$('#capture').on(
'click',
e => {
console.log('Hello!')
},
{ capture: true }
)
$('#bubbling-stop-button-2').on('click', e => {
e.stopPropagation()
console.log("I'm bubbling-stop-button-2")
})
</script>
위 코드는 버튼 안에 stopPropagation() 이 있더라도 “Hello!” 가 출력된다.
왜냐하면 div 태그의 이벤트에 { capture: true } 로 인하여 div 태그가 Capturing 단계일 때 “Hello!”가 먼저 출력되고, 그 후 “I’m bubbling-stop-button-2”가 출력된다.
3. 이벤트 전파를 막음으로써 하위 요소의 이벤트 실행 막기
<div id="capture-stop">
<button id="prevented-button">Stop</button>
</div>
<script>
$('#capture-stop').on(
'click',
e => {
e.stopPropagation()
console.log('I prevent capturing phase')
},
{ capture: true }
)
$('#prevented-button').on('click', e => {
console.log("I'm prevented-button (will not trigger)")
})
</script>
위 코드에서 button 태그의 이벤트는 수행되지 않는다.
capture: true 로 설정되어 있고, stopPropagation() 함수가 이벤트 전파를 막기 때문이다.
preventedButton.click()
click() 함수로 강제로 실행하기도 불가능하다.
상위 요소에서 이벤트 전파를 막고 있기 때문에 이벤트가 전달되지 않아 이벤트 핸들러가 수행되지 않는다.
4. 이벤트 중간에 멈추기
stopPropagation메서드는 다음 Phase로의 이벤트 전파를 막는다.
Event Phase 중 Target Phase에서 이벤트 전파를 막기 위해서는 stopImmediatePropagation 메서드를 사용할 수 있다.
<button id="multiclick-button">multi</button>
<script>
const el = $('#multiclick-button')
el.on('click', e => {
console.log(`first click`)
})
el.on('click', e => {
console.log(`second click`)
e.stopImmediatePropagation()
})
el.on('click', e => {
console.log(`third click`)
})
el.on('click', e => {
console.log(`fourth click`)
})
// console: first click
// console: second click
</script>
위 코드는 “first click”과 “second click” 까지만 수행된다.
stopImmediatePropagation 메서드가 이후의 이벤트 실행을 막기 때문이다.
'CS' 카테고리의 다른 글
[JS] 클로저(Closure)의 개념과 React에서의 활용법 (0) | 2025.04.13 |
---|---|
HTTP 프로토콜과 HTTP 프로토콜의 진화 과정(0.9~3.0) (0) | 2025.03.28 |
웹 브라우저의 동작 원리와 웹 최적화 (2) | 2025.03.25 |
[JS] 스코프에 대하여 (0) | 2024.01.03 |