액츄얼 돔 노드에 장착된 이벤트 핸들러는 최상단 window
노드로부터 해당 노드까지 이벤트가 전파되는 3가지 단계를 거친다.
이벤트 핸들러가 이벤트를 캐치하는 순간은 바로 버블링 단계에서 일어난다.[2]
<body>
<div id="container">
<button>여기를 클릭하세요!</button>
</div>
<pre id="output"></pre>
</body>
<script>
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `${e.currentTarget.tagName} 요소를 클릭했습니다.\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
</script>
이벤트가 발생한 노드까지 도달하기 위한 캡쳐페이즈가 존재하는 것은 상식적으로 이해가 간다.
DOM 자체가 트리 구조의 객체이고 트리 구조의 객체에서 이벤트가 발생한 노드까지 탐색하여 이벤트를 전달해야 하니까 말이다.
그렇다면 의문이 든다.
그럼 이벤트 버블링 단계는 왜 필요할까 ?
사실 텍스트로만 보았을 땐 크게 와닿지 않았는데 이번에 작업하면서 좀 체감했다.
<ReferenceItem
key={url}
onClick={() => {
handleClickUrl(url);
}}
>
<ReferenceItem.Align>
<ReferenceItem.Favicon faviconUrl={faviconUrl} />
<ReferenceItem.Title>{title}</ReferenceItem.Title>
<span className="text-[0.8rem] text-gray-400 flex gap-1">
<span
className={`text-primary
${isUsed ? "" : "hidden"}`}
>
✔
</span>
[{id}]
</span>
<ReferenceItem.EraseButton id={id} title={title} />
<ReferenceItem.RemoveButton title={title} />
</ReferenceItem.Align>
{url === clickedUrl && (
...
</ReferenceItem>
다음과 같은 컴포넌트에서 ReferenceItem
에 존재하는 온클릭 이벤트 핸들러는
클릭 이벤트 발생시 특정 상태를 변경 시켜 밑에 다양한 버튼을 노출 시키는 컴포넌트이다.
이 때 생각해보면 실제 이벤트가 발생한 event.target
은 ReferenceItem (li)
태그가 아닌 내부에 존재하는 다른 자식 태그 들이다.
만약 버블링 단계가 없었다면 ReferenceItem
내부에 존재하는 모든 태그들에 모두 온클릭 이벤트를 달아줬어야 했을 것이다.
하지만 버블링 단계에서 부모 태그가 자식 태그에서 발생한 이벤트를 캡쳐 할 수 있게 되어 자식 태그에서 핸들링 해야 할 이벤트를
부모태그 한 곳에서만 핸들링 하여 이벤트 처리를 위임 해줄 수 있게 되었다.
이벤트의 전파를 막기 위해선 이벤트 핸들러에서 이벤트 객체에게 stopPropagation()
메소드를 호출해줘야 한다.
이벤트 전파 (propagation)가 막힌 노드는 이벤트의 전달 방향이 캡쳐링 단계건 버블링 단계건 상관 없이 더 이상 이벤트를 전파 하지 않는다.
그럼 이런 경우가 언제 필요 할까?
<ReferenceItem
key={url}
onClick={() => {
handleClickUrl(url);
}}
>
...
<ReferenceItem.EraseButton id={id} title={title}/>
<ReferenceItem.RemoveButton title={title} />
...
</ReferenceItem>
주로 동일한 이벤트 핸들러가 부모와 자식이 모두 갖는 경우에 해당한다.
아이템에 붙어 있는 클릭 이벤트 핸들러 뿐 아니라 자식에 존재하는 ...Button
컴포넌트도 클릭 이벤트 핸들러를 가지고 있다.
export const Button = ({
children,
className = "",
size = "md",
onClick,
...props
}: ButtonProps) => {
return (
<button
{...props}
...
onClick={onClick}
>
{children}
</button>
);
};
이벤트 전파를 막지 않은 채로 해당 버튼이 클릭 되면 어떤 일이 일어나는지 보자
나는 자식 노드의 이벤트 핸들러만 발생하길 기대하였는데 (해당 아이템이 상 하로 이동하는 행위)
버튼에서 캡쳐 된 이벤트가 부모 이벤트로 버블링 되면서 부모 이벤트 또한 발생하는 모습을 볼 수 있다.
이러한 문제를 방지하기 위해 이벤트 핸들러에서 전파 과정을 막도록 다음처럼 작성해줄 수 있다.
export const Button = ({
children,
className = "",
size = "md",
onClick,
...props
}: ButtonProps) => {
return (
<button
{...props}
...
onClick={(e) => {
e.stopPropagation();
if (onClick) {
onClick(e);
}
}}
>
{children}
</button>
);
};