이벤트 흐름과 버블링 (Event Bubbling & Capturing)
💡 이벤트의 3단계 흐름 (Event Flow)
DOM 이벤트는 단방향으로만 전달되지 않고, 문서 최상단부터 타깃 요소까지 내려갔다가 다시 올라오는 과정을 거칩니다. 이를 이벤트 흐름이라고 부르며, 실무에서 이벤트 위임(Event Delegation)이나 이벤트 전파 방지(e.stopPropagation())를 이해하는 핵심 원리입니다.
- 1. 캡처링 단계 (Capturing Phase): 이벤트가 최상위
Window부터 타깃 요소의 부모까지 안쪽으로 파고들며 탐색합니다. - 2. 타깃 단계 (Target Phase): 이벤트가 실제 타깃 요소(버튼)에 도달하여 콜백 함수를 실행합니다.
- 3. 버블링 단계 (Bubbling Phase): 타깃에서부터 다시 최상위
Window까지 바깥으로 빠져나가며 전파됩니다.
<!-- 3단 중첩 구조를 통한 버블링 시각화 예제 -->
<h4 style="margin-top: 0;">🎈 3단 중첩 이벤트 버블링 테스트</h4>
<p style="font-size: 0.9em; color: #94a3b8; margin-bottom: 15px;">가장 안쪽의 <b>Box 3</b>를 클릭해서 이벤트가 위로 어떻게 전달되는지 관찰해 보세요.</p>
<div id="box1" style="padding: 20px; background-color: #0f172a; border: 2px dashed #3b82f6; border-radius: 8px; cursor: pointer;">
<span style="color: #60a5fa; font-weight: bold; pointer-events: none;">Box 1 (할아버지)</span>
<div id="box2" style="padding: 20px; background-color: #1e293b; border: 2px dashed #10b981; border-radius: 8px; margin-top: 10px; cursor: pointer;">
<span style="color: #34d399; font-weight: bold; pointer-events: none;">Box 2 (아버지)</span>
<div id="box3" style="padding: 20px; background-color: #334155; border: 2px solid #f59e0b; border-radius: 8px; margin-top: 10px; cursor: pointer; text-align: center;">
<span style="color: #fbbf24; font-weight: bold; font-size: 1.1em; pointer-events: none;">Box 3 (나) - 나를 클릭하세요!</span>
</div>
</div>
</div>
<div style="margin-top: 15px; display: flex; gap: 10px;">
<button id="clearBtn" style="background-color: #475569; color: white; border-radius: 4px; padding: 5px 10px; font-size: 0.8em;">로그 지우기</button>
<button id="stopBtn" style="background-color: #ef4444; color: white; border-radius: 4px; padding: 5px 10px; font-size: 0.8em;">Box 3 버블링 끄기</button>
</div>
<div id="bubbleOutput" style="margin-top: 15px; padding: 15px; background: #000; font-family: monospace; border-radius: 8px; min-height: 80px; font-size: 14px;"></div>
<script>
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');
const box3 = document.getElementById('box3');
const bubbleOutput = document.getElementById('bubbleOutput');
const clearBtn = document.getElementById('clearBtn');
const stopBtn = document.getElementById('stopBtn');
let isPropagationStopped = false;
function logMsg(msg, color) {
bubbleOutput.innerHTML += `<span style="color: ${color};">${msg}</span><br>`;
}
// Box 1 (최상위 부모)
box1.addEventListener('click', function(event) {
logMsg('🔵 Box 1 이벤트 실행됨!', '#60a5fa');
});
// Box 2 (중간 부모)
box2.addEventListener('click', function(event) {
logMsg('🟢 Box 2 이벤트 실행됨!', '#34d399');
});
// Box 3 (가장 안쪽 자식)
box3.addEventListener('click', function(event) {
// 버튼 토글 상태에 따라 버블링 차단
if (isPropagationStopped) {
event.stopPropagation();
logMsg('🟡 Box 3 클릭! 🛑 (버블링 차단됨)', '#fbbf24');
} else {
logMsg('🟡 Box 3 클릭! ➡️ (버블링 시작)', '#fbbf24');
}
});
// 유틸리티 버튼들
clearBtn.addEventListener('click', () => { bubbleOutput.innerHTML = ''; });
stopBtn.addEventListener('click', () => {
isPropagationStopped = !isPropagationStopped;
if (isPropagationStopped) {
stopBtn.innerText = 'Box 3 버블링 켜기';
stopBtn.style.backgroundColor = '#10b981';
} else {
stopBtn.innerText = 'Box 3 버블링 끄기';
stopBtn.style.backgroundColor = '#ef4444';
}
});
</script>