” 매일메일 프론트엔드 문제입니다.
핵심 요약
이벤트 버블링
은 이벤트가 발생했을 때 가장 안쪽의타겟
요소에서 시작해 부모 요소로 전파되는 것입니다.이벤트 캡처링
은 이와 반대로 이벤트가 루트 오소에서 시작해 타겟 요소로 내려오면서 전파됩니다. 버블링을 활용해 이벤트 위임 같은 패턴을 사용할 수 있습니다. 이벤트 버블링을 제어하기 위해 stopPropagation이나 preventDefault 메서드를 사용할 수 있습니다.
🛠️문제 상황 : 실제 프로젝트에서 발생한 이벤트 전파 이슈
실제 프로젝트를 진행하면서 다음과 같은 기능 요구사항이 있었습니다:
- 리뷰 댓글 컴포넌트의 댓글 아이콘 클릭 시, 댓글 창이 토글(toggle) 되어야 했고
- 전체 리뷰 컴포넌트를 클릭하면 해당 영화 상세페이지로 이동해야 했습니다.
하지만 댓글 아이콘 클릭 시, 댓글창도 열리고 상세페이지로도 이동해버리는 문제가 발생했습니다. 즉, 내부 이벤트가 외부로 전파되며 원치 않는 동작이 발생한 것이죠.
e.stopPropagation()을 통해 이벤트 전파를 막아서 위의 원하던 기능 요구사항을 수행할 수 있었다.
❓ 왜 이런 일이 발생했을까?
이 모든 문제는 버블링(Bubbling) 현상 때문입니다.
🔁 이벤트 버블링이란?
한 요소에 이벤트가 발생하면, 이 요소에 할당된
핸들러
가 동작하고 이어서 부모 요소의 핸들러가 동작합니다. 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작합니다.
TIP🧐 핸들러(Handler)란?
onClick
,addEventListener('click', ...)
등이 바로 핸들러입니다. 어떤 이벤트가 발생했을 때 실행되는 함수를 말합니다.
📄 예시 코드
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
p
태그를 클릭하면 다음과 같은 순서로 핸들러가 실행됩니다:
p
→div
→form
→document
…
버블링은 마치 깊은 곳에서 버블이 올라오듯, 안쪽 요소에서 바깥쪽으로 이벤트가 전파되는 구조입니다.
CAUTION거의 모든 이벤트는 버블링된다. focus 이벤트는 아니긴 하지만… 대부분 버블링된다.
event.target와 this(=event.currentTarget)
event.target
로 이벤트가 정확히 어떤 요소에서 발생했는지 알 수 있다.
이벤트가 발생한 가장 안쪽의 요소는 타깃 target 요소라고 불리고, event.target를 사용해 접근할 수 있다.
event.target와 this(=event.currentTarget)의 차이점
어떤 차이가 있나면 전자는 실제 이벤트가 시작된 타깃 요소이다. 버블링이 진행되어도 여기서 시작되었다는 것은 변하지 않는다.
반면에, this는 현재 요소
로 현재 실행 중인 핸들러가 할당된 요소를 참조한다.
<form onclick="alert('form')">FORM
<div>DIV
<p>P</p>
</div>
</form>
form
에만 이벤트 핸들러가 있어도, 자식인 div
나 p
를 클릭하면 이벤트가 버블링되어 form
의 핸들러가 실행됩니다.
이때 event.target
은 실제로 클릭한 요소(div
나 p
)이고, this
는 핸들러가 연결된 요소인 form
을 가리킵니다.
이벤트 전파는 왜 존재할까?
브라우저에서 이벤트는 캡처링 → 타깃 → 버블링 단계로 전파됩니다.
이처럼 루트에서 시작해 타깃까지 내려갔다가 다시 부모로 올라오는 구조는 이유 없이 설계된 것이 아닙니다.
1. 논리적 이유
자식 요소는 부모 요소의 영역 안에 포함되어 있기 때문에, 자식을 클릭하는 행위는 곧 부모를 클릭하는 행위로도 해석할 수 있습니다.
따라서 자식 요소에서 발생한 이벤트가 부모에게 전파되는 건 자연스러운 구조입니다.
2. 성능적 이유
이벤트를 모든 자식 요소마다 등록하는 대신, 부모 요소 하나에만 등록해도 자식 요소의 이벤트를 처리할 수 있습니다.
이런 방식은 이벤트 위임(Event Delegation)이라고 부르며, DOM 요소가 많을수록 성능과 유지보수 면에서 효율적입니다.
🧭 이벤트 전파 흐름: 캡처링 → 타깃 → 버블링
전파 단계
표준 DOM 이벤트에서 정의한 이벤트 흐름의 3단계
- 캡처링 단계: 이벤트가 루트에서 시작하여 타깃까지 내려갑니다
- 타깃 단계: 이벤트가 실제 발생한 요소에서 동작합니다
- 버블링 단계: 타깃 요소에서 부모 요소로 올라갑니다
대부분의 이벤트는 버블링만 활성화되어 있음.
focus
,blur
등은 예외적으로 버블링되지 않음
<td>
를 클릭하면 어떻게 동작할까?
<td>
요소를 클릭하면, 이벤트는 다음과 같은 3단계를 거칩니다:
- 캡처링 단계
이벤트가 **최상위 루트 요소(document)**에서 시작되어, 클릭된 요소까지 아래로 전파됩니다. - 타깃 단계
이벤트가 실제로 클릭된 **타깃 요소(td)**에 도달하여, 이 요소에 등록된 핸들러가 실행됩니다. - 버블링 단계
이벤트는 다시 타깃 요소에서 부모 방향으로 위로 전파되며, 각 조상 요소에 등록된 핸들러가 실행됩니다.
👉 이처럼 전파되는 과정 속에서 각 단계에 등록된 이벤트 핸들러들이 차례대로 실행되는 것이 바로 이벤트 전파의 전체 구조입니다.
활용 예시
버블링과 캡처링은 이벤트 위임(event delegation)의 토대가 된다. 이벤트 위임은 강력한 이벤트 핸들링 패턴이다.
- (todo) 이벤트 위임
🛑 언제 버블링을 멈춰야 할까?
버블링은 대부분의 경우 유용하지만, 특정 상황에서는 오히려 문제가 되기도 합니다.
앞서 소개한 댓글 버튼 클릭 시 상세페이지로 이동하는 문제처럼, 자식 요소의 이벤트가 부모까지 전파되면 안 되는 경우에는 버블링을 막아야 합니다.
🧩 버블링 중단 방법: event.stopPropagation()
이벤트 버블링을 막기 위해서는 이벤트 객체의 stopPropagation()
메서드를 사용합니다.
<body onclick="alert('버블링은 여기까지 도달하지 못합니다.')">
<button onclick="event.stopPropagation()">클릭해 주세요.</button>
</body>
위 예시에서 버튼을 클릭해도 body
에 등록된 onclick
핸들러는 실행되지 않습니다.
즉, 버튼 → body로 이어지는 버블링이 중단된 것입니다.
🛑 stopImmediatePropagation()
의 차이점
stopPropagation()
은 이벤트 전파(버블링/캡처링)만 중단합니다.
하지만 하나의 요소에 여러 개의 이벤트 핸들러가 등록된 경우, 다른 핸들러는 여전히 실행될 수 있습니다.
이런 상황에서는 stopImmediatePropagation()
을 사용해야 합니다.
⚠️ 버블링을 중단할 때는 반드시 신중하게
event.stopPropagation()
은 정말 필요한 경우가 아니라면 사용을 자제하는 것이 좋습니다.
잘못 사용하면 의도하지 않게 다른 기능이나 시스템과의 충돌이 발생할 수 있습니다.
📉 실제 문제 예시: dead zone 발생
- 중첩 메뉴 구현
- 각 서브 메뉴 항목에 클릭 이벤트를 등록하고, 상위 메뉴의 이벤트가 발생하지 않도록
stopPropagation()
을 사용. - 기능적으로는 문제 없지만, 다른 전역 이벤트 핸들러에 영향을 줄 수 있음.
- 각 서브 메뉴 항목에 클릭 이벤트를 등록하고, 상위 메뉴의 이벤트가 발생하지 않도록
- 행동 분석 시스템과의 충돌
- 사용자의 클릭 동선을 추적하는 시스템이
document.addEventListener('click', ...)
으로 모든 클릭 이벤트를 감지하려 함. - 하지만
stopPropagation()
이 사용된 영역에서는 이벤트가 document까지 도달하지 않음. - 이로 인해 분석 시스템이 클릭 데이터를 수집하지 못하는 dead zone이 발생.
- 사용자의 클릭 동선을 추적하는 시스템이
🧨 이렇게 되면 일부 영역은 사용자 행위 분석이 불가능해집니다.
✅ 더 나은 대안
문제상황 : “자식 요소에서 이벤트를 발생시키고 싶은데, 버블링 때문에 부모의 핸들러가 같이 실행되는 걸 막고 싶다”
그런데e.stopPropagation()
은 되도록 쓰고 싶지 않아서 다른 방법을 고민 중이다.
- 꼭 하위 요소의 이벤트 정보를 부모에게 전달하고 싶다면:
event
객체에 커스텀 데이터를 담거나CustomEvent
를 사용해 명시적으로 정보를 전달```
<div
onClick={() => console.log('부모 클릭')}
id="parent"
>
<div id="child">Click me</div>
</div>
<script>
const child = document.getElementById('child');
const parent = document.getElementById('parent');
// 부모에서 커스텀 이벤트 리스너 등록
parent.addEventListener('custom-action', (e) => {
console.log('부모가 수신한 커스텀 이벤트:', e.detail);
});
child.addEventListener('click', () => {
const customEvent = new CustomEvent('custom-action', {
detail: { message: '자식에서 발생시킨 커스텀 이벤트' },
bubbles: false // 버블링을 막음!
});
child.dispatchEvent(customEvent); // 자식이 직접 이벤트 발생
});
</script>
💡 이 방식의 핵심은?
- 자식 요소에서 이벤트를 직접 발생시키되
- 버블링 없이 커스텀 이벤트를 명시적으로 발생
- 필요하면 부모에게 명시적으로 전달하거나, 부모가 리스너로 수신만 하도록 설계 가능
결론
✅ 자식에서 발생한 이벤트가 부모를 트리거하지 않도록 하기 위해
CustomEvent
를 사용한 건 좋은 접근입니다. 특히bubbles: false
옵션을 사용하면 버블링 없이 동작하므로,stopPropagation()
없이도 전파를 막을 수 있어요.