Dev/React

[React] 오버레이 메뉴에서 이벤트 버블링 처리하기

taeyeoxn 2025. 4. 7. 05:08

• 오버레이 메뉴 구현

 

메인 화면에서 리스트 아이콘을 클릭하면 전체 메뉴가 화면 위에 오버레이 되는 기능을 구현했다. 이를 통해 사용자가 원하는 메뉴를 빠르게 탐색할 수 있도록 했다.

 

• 기본 구조

 

오버레이 메뉴는 Header 컴포넌트에서 상태를 관리하고, 해당 상태를 Menu 컴포넌트에 props로 전달하여 열고 닫는 동작을 제어하는 방식으로 구현했다.

 

Header.jsx

import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const Header = () => {
  const location = useLocation();
  const [isOpen, setIsOpen] = useState(false);

  const toggleOverlay = () => {
     setIsOpen(!isOpen);
   };

  useEffect(() => {
    // URL 경로가 변경될 때마다 메뉴 닫기
    setIsOpen(false);
  }, [location.pathname]);

  return (
    <HeaderContainer>
      <img className="logo" src={logo} alt="toyou logo" />
      <div className="btn-group">
        <DownloadButton onClick={handleDownload}>앱 다운로드</DownloadButton>
        <IconButton onClick={toggleOverlay}>
          <img src={isOpen ? closeIcon : listIcon} alt={isOpen ? '닫기' : '메뉴 열기'} />
        </IconButton>
      </div>

      {/* 메뉴 컴포넌트에 상태 전달 */}
      <Menu open={isOpen} />
    </HeaderContainer>
  );
};

 

리스트 아이콘을 클릭하면 toggleOverlay 함수가 실행되어 isOpen 상태가 토글되고, 이 값에 따라 Menu 컴포넌트가 조건부 렌더링된다.

 

또한 사용자가 메뉴에서 항목을 클릭해 다른 페이지로 이동했을 때, 메뉴가 자연스럽게 닫히도록 하기 위해 useLocation 훅의 pathname 값을 감지하여 경로가 바뀔 때마다 isOpen을 false로 설정해주는 로직도 함께 작성했다.

 

Menu.jsx

import { useNavigate } from 'react-router-dom';

const Menu = ({ open }) => {
  const nav = useNavigate();

  return (
    <OverlayMenu open={open}>
      <MenuContent>
        <p onClick={() => nav('/TeamToYou')}>Team ToYou</p>
        <p onClick={() => nav('/History')}>History</p>
        <p onClick={() => window.open('http://pf.kakao.com/_xiuPIn/chat')}>고객센터</p>
        <p onClick={() => window.open('https://forms.gle/fJweAP16cT4Tc3cA6')}>피드백하기</p>
        <p onClick={() => window.open('https://teamtoyou.tistory.com/')}>팀블로그</p>
      </MenuContent>
    </OverlayMenu>
  );
};

 

Menu 컴포넌트에서는 open 값을 받아 조건에 따라 오버레이 메뉴를 화면에 표시한다.

 

Menu.style.js

export const OverlayMenu = styled.div`
  position: fixed;
  top: 5rem;
  left: 50%;
  transform: translateX(-50%);
  width: 393px;
  height: 13.875rem;
  background-color: #ffffff;
  display: ${(props) => (props.open ? 'flex' : 'none')};
  z-index: 1;
`;

 

스타일 파일에서 open props에 따라 display 값을 동적으로 제어해 메뉴를 보여줄지 숨길지를 결정하도록 했다.

 

• 추가 기능

 

기본적인 오버레이 메뉴가 구현된 후, 사용성 향상을 위해 메뉴가 열린 상태에서 화면의 빈 공간(메뉴 영역 외부)을 클릭했을 때도  메뉴가 닫히도록 처리하고 싶었다.

const menuRef = useRef(null);

// 메뉴 열고 닫기
const toggleOverlay = () => {setIsOpen(!isOpen)};

// 메뉴 외부 클릭 시 닫기
const handleClickOutside = (e) => {
  if (menuRef.current && !menuRef.current.contains(e.target)) {
    setIsOpen(false);
  }
};

useEffect(() => {
  document.addEventListener('click', handleClickOutside);
  return () => document.removeEventListener('click', handleClickOutside);
}, []);

 

따라서 useRef를 활용해 메뉴 요소의 DOM을 참조하고, document 전체에 클릭 이벤트 리스너를 등록하여 사용자가 화면을 클릭할 때마다 메뉴 영역 외부를 클릭했는지를 판단할 수 있도록 했다.

 

메뉴가 열린 상태에서 사용자가 메뉴 외부를 클릭하면, ref.current.contains(e.target) 조건을 통해 내부 클릭 여부를 확인하고, 내부가 아닐 경우 setIsOpen(false)를 실행해 처리했다.

 

• 이슈 발생

 

기능을 추가한 후, 갑자기 메뉴 버튼을 클릭해도 메뉴가 열리지 않는 현상이 발생했다. 처음엔 로직에 문제가 있나 싶었지만, 코드를 다시 살펴보니 메뉴 버튼을 클릭한 것도 메뉴 바깥으로 인식되면서 메뉴가 열리자마자 바로 닫히고 있었던 것이 문제였다!

 

• 원인: 이벤트 버블링

 

이 현상의 원인은 바로 이벤트 버블링(Event Bubbling) 때문이었다.
이벤트 버블링이란, 웹에서 어떤 요소에 이벤트(ex. 클릭)가 발생하면 그 이벤트가 해당 요소에서 시작해 상위 부모 요소로 연속적으로 전파되는 현상을 말한다.

예를 들어, 버튼을 클릭하면 해당 버튼뿐만 아니라 그 버튼을 감싸고 있는 상위 요소들까지도 클릭 이벤트를 감지할 수 있게 된다.

 

이 상황에서 발생한 흐름은 다음과 같다:

  1. 메뉴 아이콘을 클릭하면 toggleOverlay 함수가 실행되어 isOpen 상태가 변경된다.
  2. 그런데 클릭 이벤트가 document까지 버블링되어 전파된다.
  3. handleClickOutside 함수가 document 전체에 등록되어 있어 모든 클릭을 감지한다.
  4. 이 함수가 실행되면서, 클릭한 곳이 메뉴 외부인지 판단한다.
  5. 문제는 이 시점에 메뉴가 DOM에 아직 렌더링되기 전이라 menuRef.current가 null이거나 제대로 된 영역으로 잡히지 않는다.
  6. 결국 버튼을 클릭한 것이 메뉴 외부 클릭으로 오인되어, 메뉴가 바로 닫혀버리는 현상이 발생한다.

 

• 해결 방법

 

이 문제를 해결하기 위해 두 가지 처리를 추가했다:

// 1. 이벤트 버블링 막기
const toggleOverlay = (e) => {
  e.stopPropagation(); // 클릭 이벤트가 document로 전달되지 않게 막기
  setIsOpen(!isOpen);
};
// 2. 메뉴 토글 버튼은 무시하도록 예외 처리
const handleClickOutside = (e) => {
  const toggleButton = document.querySelector('.btn-group IconButton');
  if (toggleButton && toggleButton.contains(e.target)) {
    return; // 메뉴 버튼 클릭은 무시
  }

  if (menuRef.current && !menuRef.current.contains(e.target)) {
    setIsOpen(false); // 메뉴 외부 클릭 시 닫기
  }
};

 

이렇게 이벤트 버블링을 차단하고, 메뉴 토글 버튼 클릭은 예외 처리해줌으로써 문제가 해결되었고, 메뉴가 정상적으로 열리고 닫히는 기능도 제대로 작동하게 되었다.