이벤트 버블링

고성인·2025년 4월 26일

React

목록 보기
17/17

이벤트 버블링

이벤트 버블링은 어떤 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>에서는 어떤 태그가 클릭되었는지 알 수 없다.
그렇기 때문에 이러한 상황에서 이벤트 버블링을 사용해야한다.

이벤트 버블링의 구체적인 흐름은 다음과 같다.

버튼 클릭

  1. 버튼 자체에서 onClick 이벤트 발생
  2. 이벤트가 위로 버블링됨
  3. div가 onClick 이벤트를 감지
  4. div의 onClick 핸들러 안에서 buttonElement의 textContent를 가져와서 사용한다
<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;

0개의 댓글