React, Click outside area of element.
Tags: webdev, react, javascript
Published: 2024-05-27 11:12:59


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v0m2qswsx2fjdtwt27ti.png)

이러한 메뉴 혹은 모달, 그리고 커스텀 셀렉트박스를 만들 때 외부 영역을 클릭하면 닫히게 만들고 싶은 경우가 있다. 프로젝트를 수행하면서 이를 구현한 방법을 두 가지로 나누어 구현하였다.

&nbsp;

## useState와 Overlay를 활용한 방법

메뉴나 모달이 렌더링 되는 상태를 관리하는 useState와 outside를 담당하는 Overlay 컴포넌트가 함께 렌더링 된다. 여기서 Overlay 컴포넌트 클릭 시 메뉴마 모달 컴포넌트가 언마운트 된다.

```tsx
import { useState } from "react";

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

  const toggleDropMenu = (e: React.MouseEvent<HTMLLIElement>) => {
    setDropMenuOpen((prevState) => !prevState);
  };

  return (
    <ModalWrapper>
      <ModalBtn type="button" onClick={toggleDropMenu}>
        Show Menu
      </ModalBtn>

      {isDropMenuOpen && (
        <>
          <Modal className="modal">
            {/* ...Modal Content */}
          </Modal>
          <Overlay onClick={() => setDropMenuOpen(false)} />
        </>
      )}
    </ModalWrapper>
  );
};

export default SelectComponent;
```

구현 방법은 간단하다. toggleDropMenu 함수를 통해 모달 view 여부를 토글해 주는 함수를 만들고, 버튼에는 onClick 이벤트를 적용해주고, Overlay 컴포넌트에는 setState를 false로 함수를 선언해 주면 된다.

예제 코드는 아래 CodeSandBox 사이트를 통해 테스트를 진행할 수 있다.

[예제코드보기](https://codesandbox.io/p/sandbox/react-element-outside-click-usestate-x5hs7l)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/galm8i6mpompju2rmhcs.png)

&nbsp;

## useRef와 WebAPI를 활용한 방법

모달의 객체를 메모이제이션 하는 useRef hook과, 클릭 된 이벤트 target element가  ref의 자식 요소에 포함되어 있는지 판별을 통해 outside 클릭 시 메뉴나 모달 컴포넌트가 언마운트 된다.

```tsx
import { useState, useEffect, useRef } from "react";

const SelectComponent = () => {
  const [isDropMenuOpen, setDropMenuOpen] = useState(false);
  const modalRef = useRef<HTMLElement>(null);
  const btnRef = useRef<HTMLElement>(null);

  const toggleDropMenu = (e: React.MouseEvent<HTMLLIElement>) => {
    setDropMenuOpen((prevState) => !prevState);
  };

  useEffect(() => {
    const eventCallback = (e: MouseEvent): void => {
      if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
        if (btnRef.current.contains(e.target as Node)) return;
        setDropMenuOpen(false);
      }
    };

    document.addEventListener("mousedown", eventCallback);
    return () => document.removeEventListener("mousedown", eventCallback);
  }, []);

  return (
    <ModalWrapper>
      <ModalBtn ref={btnRef} type="button" onClick={toggleDropMenu}>
        Show Menu
      </ModalBtn>
      {isDropMenuOpen && (
        <>
          <S.Modal ref={modalRef} className="modal">
            {/* ...Modal Content */}
          </S.Modal>
        </>
      )}
    </ModalWrapper>
  );
};

export default SelectComponent;
```

구현 방법은 Web API의 인스턴스 메소드은 `contains`를 활용한 것이 전부라고 해도 될 정도로 간단하다. useEffect 안에 선언된 `contains` 메소드가 클릭 이벤트를 통해 메뉴 및 모달이 포함된 element라면 return을, 아니라면 메뉴 및 모달 컴포넌트를 언마운트 한다.

```javascript
contains(otherNode)
```

> Node 인터페이스의 `contains()` 메소드는 노드가 주어진 노드의 자손인지, 즉 노드 자체인지, 직계 자식(childNodes) 중 하나인지, 자식의 직계 자식 중 하나인지 등을 나타내는 Boolean 값을 반환합니다.

예제 코드는 아래 CodeSandBox 사이트를 통해 테스트를 진행할 수 있다.

[예제코드보기](https://codesandbox.io/p/sandbox/react-element-outside-click-useref-6rqpwv)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8nai8zqoy61i15drk2ev.png)

&nbsp;

## 두 방식의 차이점

EventListener는 등록된 이벤트에 맞는 동작이 일어나도록 듣고(감시하고) 있기 때문에 removeEventListener를 적절하게 해주지 않으면 메모리 누수의 원인이 된다. 

그러나 useState를 이용한 방법은 부모 태그와 자식 태그의 위치를 이용한 방법이기 때문에 때때로 사용하기 어려운 환경일 수 있다. 그러므로 상황에 따라 적절한 방법을 선택하여 사용하는 것이 중요하다고 할 수 있다.