리액트 outside click 구현하기(feat. 이벤트 버블링)

김진영·2023년 4월 24일
2
post-thumbnail

반응형 웹페이지를 만들면서 outside click을 구현했습니다.
평소에 구현해보고 싶었던 기능이라 기분 좋게 시작했지만 중간에 약간의 잡음(?)이 있었습니다.
잡음의 원인은 '이벤트 버블링'이었는데 다시 한 번 기초 공사가 튼튼해야 함을 느꼈습니다. 😅
쉬운 듯 쉽지 않았던 그 과정을 기록해보려고 합니다.

시작

기능을 구현하기 위해 다음과 같이 접근했습니다.

  • 메뉴가 열렸는지 여부를 나타내는 useState 훅이 필요함
  • 열고 닫히는 메뉴를 참조하기 위해서는 useRef 훅도 필요함

여러 블로그 포스팅을 참조한 결과 위와 같이 기능을 구현한 코드가 대부분이었습니다.
한 가지 특이한 건 mousedown 이벤트를 사용한 코드가 많이 보였다는 점입니다.
mousedown 이벤트를 사용하면 마우스가 눌렸을 때 특정 이벤트가 동작합니다.
하지만 이는 어색한 UX로 이어질 것 같아 mousedown 대신 onClick 이벤트를 사용했습니다.

코드

이벤트 버블링을 고려하지 않고 작성한 첫 번째 코드입니다.

function Navbar() {
  const navRef = useRef();
  const [isNavOpened, setIsNavOpened] = useState(false);

  const navButtonHandler = event => {
    setIsNavOpened(prev => !prev);
  };

  useEffect(() => {
    const checkIfClickedOutside = ({ target }) => {
      if (isNavOpened && !navRef.current?.contains(target))
        setIsNavOpened(false);
    };
    window.addEventListener("click", checkIfClickedOutside);

    return () => window.removeEventListener("click", checkIfClickedOutside);
  }, [isNavOpened]);

  return (
    <div>
      ... MORE CODES ...
      <div
        onClick={navButtonHandler}
      >
        <AiOutlineMenu size={20} />
      </div>
      {isNavOpened && (
        <ul
          ref={navRef}
        >
          <li className="p-4">소개</li>
          <li className="p-4">로드맵</li>
          <li className="p-4">멘토링</li>
          <li className="p-4">커뮤니티</li>
        </ul>
      )}
    </div>
  );
}

위 코드의 내용은 다음과 같습니다.

  • isNavOpened(불리언) 값에 따라 Nav 메뉴가 생기거나 사라집니다.
  • isNavOpened 값을 업데이트 하기 위해서는 onClick 이벤트가 부착된 div 요소를 클릭해야 합니다.
  • useEffect 훅은 isNavOpened 값이 업데이트 될 때마다 실행됩니다.
  • 브라우저 창에 isNavOpened 값을 false로 업데이트 하는 클릭 이벤트를 등록합니다.
    • ('Nav 메뉴가 열려 있고 + 클릭한 대상이 Nav 메뉴에 포함되어 있지 않은 경우'에만 동작)

문제

하지만 한 가지 문제가 생겼습니다.
Nav 메뉴가 닫힌 상태에서 버튼을 아무리 클릭해도 메뉴가 열리지 않았습니다.
나름의 짱구를 굴리면서 원인을 생각해봤는데.. 역시 10번의 생각 보다 1번의 행동이 옳았습니다. 😅

우선 기존 코드에 콘솔 출력 코드를 추가했습니다.

const navButtonHandler = event => {
    // event.stopPropagation();
    console.log("div 클릭 이벤트 발생!");  // 📍
    setIsNavOpened(prev => !prev);
  };

  useEffect(() => {
    const checkIfClickedOutside = ({ target }) => {
      console.log("window 클릭 이벤트 발생!");  // 📍
      if (isNavOpened && !navRef.current?.contains(target))
        setIsNavOpened(false);
    };
    window.addEventListener("click", checkIfClickedOutside);

    return () => window.removeEventListener("click", checkIfClickedOutside);
  }, [isNavOpened]);

코드를 바꾸고 나서 Nav 메뉴를 열기 위해 버튼을 클릭했습니다.

🤨🤨🤨🤨????
🫢🫢🫢🫢!!!!!!!
사실은 버튼에 등록한 onClick 이벤트와 window에 등록한 click 이벤트가 모두 실행되고 있던 것입니다.
버튼을 클릭했을 뿐인데... 왜 window에 등록한 이벤트도 동작하는 걸까요?

🚀 이벤트 버블링

🍀 이벤트 버블링
from 하위 요소 to 상위 요소로 진행되는 이벤트 전파 방식입니다.
한 요소에 이벤트가 발생하면 해당 요소에 등록된 이벤트 핸들러가 동작하고,
곧 이어서 부모 요소에 등록된 이벤트 핸들러도 동작하는 식입니다.

허무하지만... 원인은 이벤트 버블링 때문이었습니다.
코드에서 이벤트 버블링이 발생한 과정을 살펴보겠습니다.

  1. 버튼(div 요소)을 클릭합니다.
    • isNavOpened: false 👉🏻 true
    • 기존에 false인 isNavOpened 값을 true로 업데이트.
    • 이때 버튼은 하위 요소로 이벤트가 전파되는 지점입니다.
  2. 이벤트 버블링 때문에 window에 등록된 이벤트 핸들러도 동작합니다.
    • isNavOpened: true 👉🏻 false
    • true로 업데이트 된 isNavOpened 값을 다시 false로 업데이트.
    • 이때 동작하는 이벤트 핸들러는 버튼을 클릭한 여파로 동작하는 것입니다.
    • 다시 말해서 상위 요소로 이벤트가 전파됐기 때문에 발생하는 것입니다.

결론은 isNavOpened 값이 false에서 true로, true에서 다시 false로 변했다는 것입니다.
그래서 화면에 Nav 메뉴가 나타나지 않았던 것입니다.
그러면 상위 요소로 이벤트가 전파되는 것을 방지하려면 어떡해야 할까요?

문제 해결

문제를 해결하기 위해 이벤트가 상위 요소로 전파되는 것을 막는 코드를 추가했습니다.

const navButtonHandler = event => {
    event.stopPropagation();  // 🔥 추가한 코드
    console.log("div 클릭 이벤트 발생!");
    setIsNavOpened(prev => !prev);
  };

더 이상 window에 등록한 이벤트 핸들러가 동작하지 않는 것을 확인할 수 있습니다.

이벤트 버블링에 대해서는 알고 있었지만 실제 문제로 직면한 결과 적지 않은 충격이었습니다.
지금 생각해보면 너무나도 당연한 문제였습니다.
하지만 그 당연한 문제의 원인이 왜 바로 보이지 않았을까 하며 살짝(?) 부끄럽습니다. 🙈
체계적으로 그리고 논리적으로 생각하는 습관을 더욱 길러야겠습니다!! 😊

결과

성공적으로 outside click을 구현했습니다!! 👏🏻👏🏻

profile
기록해서 남길래요

0개의 댓글