동적 웹 상호작용의 숨겨진 열쇠
웹 개발의 핵심: 이벤트 버블링 이해
웹 개발 여정 초기에 저는 이벤트 버블링이라는 흥미로운 동작을 접했습니다. 처음에는 다소 혼란스러웠지만, 그 개념을 깊이 이해하고 나니 매우 논리적이라는 것을 깨달았습니다. 웹 개발자라면 누구나 이벤트 버블링을 마주하게 될 것입니다. 그렇다면 정확히 이벤트 버블링이란 무엇일까요?
자바스크립트는 사용자 상호작용을 가능하게 하는 이벤트를 활용합니다. 이벤트는 웹 페이지에서 발생하는 특정 동작이나 사건을 의미하며, 개발자는 코드를 통해 이를 감지하고 반응할 수 있습니다. 마우스 클릭, 키보드 입력, 폼 제출 등이 대표적인 예시입니다.
자바스크립트는 이벤트 리스너를 사용하여 이러한 이벤트를 감지하고 대응합니다. 이벤트 리스너는 특정 이벤트가 발생하기를 기다리는 함수입니다. 예를 들어, 버튼 클릭 이벤트에 대한 리스너가 있을 수 있습니다. 리스너가 해당 이벤트를 감지하면, 연결된 코드를 실행하여 특정 동작을 수행합니다. 이러한 이벤트 감지 및 처리 과정을 이벤트 핸들링이라고 합니다.
이제 웹페이지에 div, span, button 세 가지 요소가 있다고 가정해 봅시다. button은 span 안에, span은 div 안에 중첩된 구조입니다. 아래 그림을 통해 이러한 구조를 확인할 수 있습니다.

각 요소에 클릭 이벤트를 감지하고, 클릭 시 콘솔에 메시지를 출력하는 이벤트 리스너가 등록되어 있다고 가정해 봅시다. 이 상황에서 button을 클릭하면 어떤 일이 발생할까요?
실제로 테스트해보려면, 새 폴더를 만들고 그 안에 index.html, style.css, app.js 파일을 생성하세요.
index.html 파일에 다음 코드를 추가합니다.
<html lang="ko">
<head>
<title>이벤트 버블링</title>
<link rel="stylesheet" href="https://wilku.top/the-hidden-key-to-dynamic-web-interactions/style.css">
</head>
<body>
<div>
<span><button>클릭하세요!</button></span>
</div>
<script src="app.js"></script>
</body>
</html>
style.css 파일에 다음 코드를 추가하여 div와 span 요소의 스타일을 지정합니다.
div {
border: 2px solid black;
background-color: orange;
padding: 30px;
width: 400px;
}
span {
display: inline-block;
background-color: cyan;
height: 100px;
width: 200px;
margin: 10px;
padding: 20px;
border: 2px solid black;
}
app.js 파일에는 div, span, button 요소에 클릭 이벤트 리스너를 추가하는 다음 코드를 추가합니다. 각 리스너는 클릭 이벤트에 반응합니다.
const div = document.querySelector('div');
div.addEventListener('click', () => {
console.log("div 요소가 클릭되었습니다.")
})
const span = document.querySelector('span');
span.addEventListener('click', () => {
console.log("span 요소가 클릭되었습니다.")
})
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log("버튼이 클릭되었습니다.")
})
이제 브라우저에서 HTML 파일을 열어보세요. 페이지를 확인한 후 버튼을 클릭하면 콘솔에 어떤 결과가 나타나는지 살펴보세요. 버튼을 클릭하면 다음과 같은 결과가 표시됩니다.

버튼을 클릭하면 해당 버튼에 연결된 클릭 이벤트 리스너가 실행됩니다. 그런데 span과 div 요소에 연결된 이벤트 리스너도 함께 실행됩니다. 왜 이런 일이 일어날까요?
버튼 클릭은 버튼에 연결된 리스너를 실행하여 콘솔에 메시지를 출력합니다. 버튼이 span 요소 내에 중첩되어 있기 때문에, 버튼 클릭은 기술적으로 span 요소도 클릭한 것이 됩니다. 따라서 span 요소에 연결된 이벤트 리스너도 실행됩니다.
span 요소 역시 div 요소 내에 중첩되어 있으므로 span 클릭은 div 클릭으로 이어지고, 결과적으로 div 요소의 이벤트 리스너도 실행됩니다. 이것이 바로 이벤트 버블링의 원리입니다.
이벤트 버블링
이벤트 버블링은 중첩된 HTML 요소 구조에서 이벤트가 발생했을 때, 가장 안쪽의 요소에서 시작하여 DOM 트리를 따라 바깥쪽 요소로 이벤트가 전파되는 현상을 의미합니다. 즉, 이벤트가 발생한 요소에서 루트 요소까지 마치 거품처럼 올라가는 것과 같은 방식으로 이벤트가 전달됩니다.
이벤트 리스너는 이벤트가 DOM 트리를 따라 '버블링'되는 방식에 따라 특정 순서로 실행됩니다. 이 글에서 사용한 HTML 구조를 바탕으로 아래 표시된 DOM 트리를 살펴보겠습니다.
DOM 트리를 따라 버블링되는 클릭 이벤트
DOM 트리는 div 내부에 span, 그 안에 button이 중첩된 구조를 보여줍니다. 이는 body 내부에 중첩되고, body는 html 요소 내부에 중첩됩니다. button을 클릭하면 해당 요소에 연결된 리스너가 실행됩니다.
그러나 요소들이 중첩되어 있기 때문에, 이벤트는 DOM 트리 위로 span 요소, div, body, 그리고 최종적으로 html 요소까지 거슬러 올라가면서 클릭 이벤트를 감지하는 모든 리스너를 순서대로 실행합니다.
이것이 span과 div 요소에 연결된 이벤트 리스너가 실행되는 이유입니다. 만약 body나 html 요소에 클릭을 감지하는 리스너가 있다면 이들 또한 실행될 것입니다.
이벤트가 발생하는 DOM 노드를 '타겟'이라고 부릅니다. 이 경우에는 버튼에서 클릭 이벤트가 발생했으므로 버튼 요소가 이벤트의 타겟입니다.
이벤트 버블링 중단 방법
이벤트가 DOM에서 버블링되는 것을 막기 위해서는 이벤트 객체에서 사용할 수 있는 stopPropagation() 메소드를 사용해야 합니다. 버튼에 이벤트 리스너를 추가하는 데 사용했던 아래 코드를 살펴봅시다.
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log("버튼이 클릭되었습니다.");
})
이 코드는 사용자가 버튼을 클릭할 때 DOM 트리를 따라 버블링되는 이벤트를 발생시킵니다. 이벤트 버블링을 막기 위해 아래와 같이 stopPropagation() 메소드를 호출합니다.
const button = document.querySelector('button');
button.addEventListener('click', (e) => {
console.log("버튼이 클릭되었습니다.");
e.stopPropagation();
})
이벤트 핸들러는 버튼을 클릭할 때 실행되는 함수입니다. 이벤트 리스너는 자동으로 이벤트 객체를 이벤트 핸들러에 전달합니다. 이 이벤트 객체는 e라는 변수 이름으로 전달됩니다.
이 이벤트 객체는 이벤트에 대한 정보를 담고 있으며, 다양한 속성과 메소드에 접근할 수 있게 해줍니다. 그 중 하나가 stopPropagation() 메소드로, 이벤트 버블링을 방지하는 데 사용됩니다. 버튼의 이벤트 리스너에서 stopPropagation()을 호출하면 이벤트가 버튼 요소에서 DOM 트리로 버블링되는 것을 막을 수 있습니다.
stopPropagation() 메소드를 추가한 후 버튼을 클릭하면 다음과 같은 결과가 출력됩니다.

stopPropagation()을 사용하면 이벤트가 특정 요소에서 멈추도록 할 수 있습니다. 예를 들어, 클릭 이벤트가 버튼에서 span 요소까지만 버블링되고 DOM 트리 위로 더 이상 전파되지 않도록 하려면 span의 이벤트 리스너에서 stopPropagation()을 사용하면 됩니다.
이벤트 캡처
이벤트 캡처는 이벤트 버블링의 반대되는 개념입니다. 이벤트 캡처에서는 이벤트가 가장 바깥쪽 요소에서 시작하여 대상 요소까지 DOM 트리를 따라 흘러 내려갑니다. 아래 그림은 이러한 이벤트 캡처 과정을 보여줍니다.
이벤트 캡처를 통해 클릭 이벤트가 대상 요소로 흘러 내려갑니다.
예를 들어, 버튼 요소를 클릭하면 이벤트 캡처 단계에서는 div 요소의 이벤트 리스너가 가장 먼저 실행됩니다. 그 다음 span 요소의 리스너가 실행되고 마지막으로 이벤트 타겟 요소의 리스너가 실행됩니다.
그러나 이벤트 버블링은 DOM에서 이벤트가 전파되는 기본 방식입니다. 이벤트 버블링에서 이벤트 캡처로 기본 동작을 바꾸려면, 이벤트 리스너에 세 번째 인수를 전달하여 capture 속성을 true로 설정해야 합니다. 세 번째 인수를 전달하지 않으면 capture 속성은 기본적으로 false로 설정됩니다.
아래 이벤트 리스너를 예로 들어 보겠습니다.
div.addEventListener('click', () => {
console.log("div 요소가 클릭되었습니다.")
})
세 번째 인수가 없으므로 capture는 false로 설정되어 있습니다. capture를 true로 설정하려면 세 번째 인수로 불리언 값 true를 전달합니다.
div.addEventListener('click', () => {
console.log("div 요소가 클릭되었습니다.")
}, true)
또는 다음과 같이 capture 속성이 true인 객체를 전달할 수도 있습니다.
div.addEventListener('click', () => {
console.log("div 요소가 클릭되었습니다.")
}, {capture: true})
이벤트 캡처를 테스트하려면 JavaScript 파일에서 모든 이벤트 리스너에 세 번째 인수를 추가하십시오.
const div = document.querySelector('div');
div.addEventListener('click', () => {
console.log("div 요소가 클릭되었습니다.")
}, true)
const span = document.querySelector('span');
span.addEventListener('click', (e) => {
console.log("span 요소가 클릭되었습니다.")
}, true)
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log("버튼이 클릭되었습니다.");
}, true)
이제 브라우저를 열고 버튼 요소를 클릭해보세요. 다음과 같은 결과가 출력될 것입니다.

이벤트 버블링에서는 버튼의 출력이 먼저 나타나는 것과 달리, 이벤트 캡처에서는 가장 바깥쪽 요소인 div의 출력이 먼저 나타납니다.
이벤트 버블링과 이벤트 캡처는 DOM에서 이벤트가 전파되는 주요 방식입니다. 하지만 이벤트 버블링이 일반적으로 이벤트 전파에 사용되는 방식입니다.
이벤트 위임
이벤트 위임은 하위 요소 각각에 이벤트 리스너를 추가하는 대신, 공통 부모 요소(예: ul 요소)에 하나의 이벤트 리스너를 연결하는 디자인 패턴입니다. 하위 요소에서 발생한 이벤트는 부모 요소까지 버블링되고, 부모 요소의 이벤트 핸들러에서 처리됩니다.
따라서 하위 요소를 포함하는 부모 요소가 있는 경우, 부모 요소에만 이벤트 리스너를 추가합니다. 이 하나의 이벤트 핸들러가 모든 하위 요소의 이벤트를 처리합니다.
부모 요소가 어떤 자식 요소가 클릭되었는지 어떻게 알 수 있을까요? 앞에서 언급했듯이 이벤트 리스너는 이벤트 객체를 이벤트 핸들러에 전달합니다. 이 이벤트 객체는 특정 이벤트에 대한 정보를 제공하는 메소드와 속성을 포함하고 있습니다. 이벤트 객체의 속성 중 하나가 target 속성입니다. target 속성은 이벤트가 발생한 특정 HTML 요소를 가리킵니다.
예를 들어, 목록 항목이 있는 ul 요소가 있고, 목록 항목에서 이벤트가 발생할 때 ul 요소에 이벤트 리스너를 연결하면, 이벤트 객체의 target 속성은 이벤트가 발생한 특정 목록 항목을 가리킵니다.
이벤트 위임이 작동하는 것을 확인하려면 기존의 HTML 파일에 다음 HTML 코드를 추가해 보세요.
<ul> <li>Toyota</li> <li>Subaru</li> <li>Honda</li> <li>Hyundai</li> <li>Chevrolet</li> <li>Kia</li> </ul>
이벤트 위임을 사용하여 부모 요소의 단일 이벤트 리스너로 하위 요소의 이벤트를 감지하려면 다음 JavaScript 코드를 추가하십시오.
const ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
// 타겟 요소
targetElement = e.target
// 타겟 요소의 내용 출력
console.log(targetElement.textContent)
})
이제 브라우저를 열고 목록에 있는 항목을 클릭하십시오. 콘솔에 요소의 내용이 아래와 같이 출력됩니다.

단 하나의 이벤트 리스너를 사용하여 모든 하위 요소의 이벤트를 처리할 수 있습니다. 페이지에 너무 많은 이벤트 리스너가 존재하면 성능에 부정적인 영향을 미치고 메모리 사용량이 증가하며 페이지 로드 및 렌더링 속도가 느려질 수 있습니다.
이벤트 위임을 사용하면 페이지에서 필요한 이벤트 리스너 수를 최소화하여 이 모든 문제를 피할 수 있습니다. 이벤트 위임은 이벤트 버블링에 의존합니다. 즉, 이벤트 버블링이 웹 페이지 성능을 최적화하는 데 도움이 될 수 있다고 말할 수 있습니다.
효율적인 이벤트 처리 팁
개발자로서 DOM에서 이벤트를 다룰 때는 페이지의 요소에 많은 이벤트 리스너를 추가하는 대신 이벤트 위임을 사용하는 것을 고려해 보세요.

이벤트 위임을 사용할 때에는 이벤트 처리가 필요한 하위 요소의 가장 가까운 공통 조상에 이벤트 리스너를 연결해야 합니다. 이는 이벤트 버블링을 최적화하는 데 도움이 되며, 이벤트가 처리되기 전 이동해야 하는 경로도 최소화합니다.
이벤트를 처리할 때 이벤트 리스너가 제공하는 이벤트 객체를 최대한 활용해야 합니다. 이벤트 객체에는 이벤트 처리에 유용한 target과 같은 속성이 포함되어 있습니다.
웹사이트 성능을 향상시키려면 과도한 DOM 조작을 피해야 합니다. 빈번한 DOM 조작을 유발하는 이벤트가 있으면 웹 사이트 성능에 부정적인 영향을 미칠 수 있습니다.
마지막으로, 중첩된 요소의 경우 중첩된 이벤트 리스너를 요소에 연결할 때 매우 주의해야 합니다. 이는 성능에 영향을 미칠 뿐만 아니라 이벤트 처리를 매우 복잡하게 만들고 코드를 유지 관리하기 어렵게 만듭니다.
결론
이벤트는 자바스크립트의 강력한 도구입니다. 이벤트 버블링, 이벤트 캡처, 이벤트 위임은 자바스크립트에서 이벤트를 처리하는 데 중요한 도구입니다. 웹 개발자로서 이러한 개념을 잘 이해하면 더욱 상호작용적이고 동적이며 고성능 웹사이트와 애플리케이션을 구축할 수 있습니다.