맨 땅에 프론트엔드 개발자 되기

React 외부 영역 클릭 시 닫기 본문

코딩 공부 일지/React JS

React 외부 영역 클릭 시 닫기

헬로코딩 2022. 11. 30. 12:07
728x90

이러한 메뉴 혹은 모달, 그리고 커스텀 셀렉트박스를 만들 때, 외부 영역을 클릭하면 닫히게 만들고 싶을 경우, 사용하는 방법은 크게 두 가지가 있다.

useState 활용하기

import React, { useState } from "react";

const Sample = () => {
  const [isDropMenuOpen, setDropMenuOpen] = useState(false);

  const toggleDropMenu = (e: React.MouseEvent<HTMLLIElement>) => {
    e.stopPropagation(); // 이벤트 캡쳐링 방지
    setDropMenuOpen(prevState => !prevState);
  }

  return (
    <SampleContainer
    	// 전체 영역 태그
        // 전체 영역 아무 곳을 클릭해도 드롭메뉴가 닫힘
      onClick={() => setDropMenuOpen(false)}
    >
      <div className="sample-header">
        <ul className="sample-header-menu">
          <li className="sample-header-menu-item">
            <span className="material-symbols-outlined icon">print</span>
          </li>
          <li className="sample-header-menu-item">
            <span className="material-symbols-outlined icon">download</span>
          </li>
          <li className="sample-header-menu-item"
          	// 토글버튼에 토글기능 넣어주기
            onClick={toggleDropMenu}
          >
            <span className="material-symbols-outlined icon">more_vert</span>
          </li>
        </ul>
        {isDropMenuOpen && (
          <ul className="sample-header-dropmenu">
            <li className="sample-header-dropmenu-item">
              <span className="material-symbols-outlined icon">link</span>
              <span className="text">링크 복사</span>
            </li>
          </ul>
        )}
      </div>
    </SampleContainer>
  )
}

export default Sample;

useState 만을 활용하는 방법은 버튼에 useState로 토글기능을 주고, 전체를 감싸고 있는 태그 위치에 state를 false로 만들도록 하는 것이다. 여기서 중요한 점은 이 방법을 사용할 경우에는 이벤트 캡쳐링이 발생하므로 이벤트 캡쳐링을 방지하는 코드를 꼭 넣어주어야 한다는 것이다. 이벤트 버블링과 캡쳐링에 대해 모른다면 꼭 알아두어야 하는 개념이니 한번 살펴보면 좋다.
 
위의 방법은 경우에 따라 사용하기가 어려울 수도 있다. 그럴 경우에는 addEventListener를 이용하는 방법도 있다.

addEventListener 이용하기

import React, { useEffect, useRef, useState } from "react";

const Sample = () => {
  const dropMenuRef = useRef<HTMLDivElement | null>(null);
  const [isDropMenuOpen, setDropMenuOpen] = useState<boolean>(false);

  useEffect(() => {
    const handleOutsideClose = (e: {target: any}) => {
    	// useRef current에 담긴 엘리먼트 바깥을 클릭 시 드롭메뉴 닫힘
      if(isDropMenuOpen && (!dropMenuRef.current.contains(e.target))) setDropMenuOpen(false);
    };
    document.addEventListener('click', handleOutsideClose);
    
    return () => document.removeEventListener('click', handleOutsideClose);
  }, [isDropMenuOpen]);

  return (
    <header>
      <nav className="navContainer">
          <div ref={dropMenuRef} className="dropmenu-wrapper">
            <div className="userNameContainer"
              onClick={() => setDropMenuOpen(!isDropMenuOpen)}
            >
              {isDropMenuOpen ? (
                <span className="material-symbols-outlined icon">arrow_drop_up</span>
              ) : (
                <span className="material-symbols-outlined icon">arrow_drop_down</span>
              )}
            </div>
            {isDropMenuOpen && (
              <div className="dropmenu">
                <ul className="dropmenu-list">
                  <li className="dropmenu-list-item"
                    onClick={handleLogout}
                  >
                    <span className="logout-btn">로그아웃</span>
                  </li>
                </ul>
              </div>
            )}
          </div>
      </nav>
    </header>
  )
}

export default Sample;

먼저 외부영역이 아닌 곳에 useRef 를 이용하여 current 에 엘리먼트를 담는다. 그리고 addEventListener를 이용하여 useRef에 담긴 엘리먼트가 아닌 곳을 클릭했을 때 드롭메뉴를 닫는 이벤트를 등록해준다! useEffect 안에서 등록한 이벤트는 컴포넌트의 마운트가 해제될 때 이벤트도 삭제되어 성능을 향상시킬 수 있도록 꼭 리턴 값에 removeEventListener로 이벤트를 삭제하도록 하자.

두 방법의 차이점

EventListener는 말그대로 등록된 이벤트에 맞는 동작이 일어나도록 듣고(감시하고) 있기 때문에 removeEventListener를 적절하게 해주지 않으면 메모리 누수의 원인이 된다. 그러나 useState를 이용한 방법은 부모 태그와 자식 태그의 위치를 이용한 방법이기 때문에 때때로 사용하기 어려운 환경일 수 있다. 그러므로 상황에 따라 적절한 방법을 택하면 될 것 같다.

728x90