이벤트 버블링은 어떤 HTML 요소에서 이벤트가 발생했을 때, 그 요소에서 시작해서 부모, 조부모, 최상위까지 차례대로 이벤트가 전파되는 것을 의미한다.
import { ReactNode } from "react";
interface TagButtonProps {
children: ReactNode;
isChecked: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
}
const TagButton = ({ children, isChecked, onClick }: TagButtonProps) => {
const buttonStyle = isChecked ? "bg-white text-primary" : "text-white bg-dark-opacity";
return (
<button
className={`
rounded-tag-button
px-[10px] border
border-white
h-[33px]
text-sm
font-medium
${buttonStyle}
`}
onClick={onClick}
>
{children}
</button>
);
};
export default TagButton;
예를들어 위와 같은 <TagButton> 컴포넌트를 사용하여 여러 태그들이 존재하는 <TagList>컴포넌트를 만든다고 해보자.
import { useState } from "react";
import TagButton from "./TagButton";
interface ITagListProps {
tagList: string[];
onTagClick: (tag: string) => void;
}
const TagList = ({ tagList, onTagClick }: ITagListProps) => {
const [selectedTag, setSelectedTag] = useState<string>(tagList[0]);
return (
<div className="flex gap-x-4">
{tagList.map((tag) => (
<TagButton key={tag} isChecked={tag === selectedTag} onClick={() => setSelectedTag(tag)}>
{tag}
</TagButton>
))}
</div>
);
};
export default TagList;
위의 코드를 살펴보면 <div>태그 안에 props로 전달받은 문자 배열을 통해 <TagButton>들을 만들어주는 컴포넌트이다.
만들어진 <TagList>내부의 <TagButton>을 클릭하게 되면 클릭된 버튼의 스타일이 <TagButton>의 isChecked을 통해 변화하게 된다.
이때 props로 전달받은 onTagClick이 실행되는 부분은 <TagButton>컴포넌트가 아니라 현재 컴포넌트인 <TagList>컴포넌트이다.
따라서 <TagButton>에 새로운 함수를 만들어서 전달하는것이 아닌 <TagList>에서 처리해야하기 때문에 해당 이벤트는 <div>태그에 전달되어야 한다.
import { useState } from "react";
import TagButton from "./TagButton";
interface ITagListProps {
tagList: string[];
onTagClick: (tag: string) => void;
}
const TagList = ({ tagList, onTagClick }: ITagListProps) => {
const [selectedTag, setSelectedTag] = useState<string>(tagList[0]);
return (
<div className="flex gap-x-4" onClick={onTagClick}>
{tagList.map((tag) => (
<TagButton key={tag} isChecked={tag === selectedTag} onClick={() => setSelectedTag(tag)}>
{tag}
</TagButton>
))}
</div>
);
};
export default TagList;
하지만 위와 같이 작성을 하게되면 onTagClick은 tag를 전달받아야 하지만 <div>에서는 어떤 태그가 클릭되었는지 알 수 없다.
그렇기 때문에 이러한 상황에서 이벤트 버블링을 사용해야한다.
이벤트 버블링의 구체적인 흐름은 다음과 같다.
버튼 클릭
<div
className="flex gap-x-4"
onClick={(event) => {
if (event.target instanceof HTMLButtonElement) {
const tag = event.target.textContent;
onTagClick(tag);
}
}}
>
event.target instanceof HTMLButtonElement를 통해 버블링된 이벤트가 버튼일 경우에만 onTagClick에 전달해준다.
하지만 위와같이 작성하면 event.target.textContent가 null일수도 있기 때문에 타입 에러가 발생한다.
이 경우 제네릭 타입을 사용하여 tagList로 넘어온 아이템이 onTagClick에 전달된다는 것을 보증할수 있다.
import { useState } from "react";
import TagButton from "./TagButton";
interface ITagListProps<T extends string> {
tagList: T[];
onTagClick: (tag: T) => void;
}
const TagList = <T extends string>({ tagList, onTagClick }: ITagListProps<T>) => {
const [selectedTag, setSelectedTag] = useState<T>(tagList[0]);
return (
<div
className="flex gap-x-4"
onClick={(event) => {
if (event.target instanceof HTMLButtonElement) {
const tag = event.target.textContent as T;
onTagClick(tag);
}
}}
>
{tagList.map((tag) => (
<TagButton key={tag} isChecked={tag === selectedTag} onClick={() => setSelectedTag(tag)}>
{tag}
</TagButton>
))}
</div>
);
};
export default TagList;