TIL37 | React | ref를 이용한 outside click 외부 클릭 감지

미연·2021년 10월 21일
17

WANTED-clone

목록 보기
1/4
post-thumbnail

모달창 외부 영역을 클릭하면 모달창이 꺼질 수 있게 구현하고 싶었다.
이 기능은 outside click이며, 이를 구현하려면 다음과 같은 개념들을 알아야 한다.

  1. 이벤트 버블링
  2. ref
  3. DOM contains method

이벤트 버블링

이벤트가 제일 깊은 곳에 있는 요소에서 시작해, 부모 요소를 거슬러 올라가며 발생하는 현상.

  1. 한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작한다.
  2. 이어서 부모 요소의 핸들러가 동작한다.
  3. 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.
<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

가장 안쪽의 <p>를 클릭하면 순서대로 다음과 같은 일이 벌어진다.

<p> -> <div> -> <form>

  1. <p>에 할당된 onclick 핸들러가 동작한다.
  2. 바깥의 <div>에 할당된 핸들러가 동작한다.
  3. 바깥의 <form>에 할당된 핸들러가 동작한다.
  4. document 객체를 만날 때까지, 각 요소에 할당된 핸들러가 동작한다.

event.target과 event.currentTarget(=this)의 차이점

  • event.target은 실제 이벤트가 시작된 타깃 요소이다. 버블링이 진행되어도 변하지 않는다.
  • event.currentTarget(=this)현재 요소로, 현재 실행중인 핸들러가 할당된 요소를 참조한다.
  • 큰 틀 중에서도 가장 안쪽에 있는 요소가 event.target라면, 이 요소를 감싸는 큰 틀이 event.currentTarget(=this)라고 할 수 있겠다.

EventTarget.addEventListener()

EventTarget.addEventListener(eventType, 함수)

  • 지정한 이벤트가 대상에 전달될 때마다 호출할 함수를 설정한다.
  • 스스로 종료되지 않기 때문에 removeEventListener를 명시해야 종료할 수 있다.
    💡 document.addEventListener : 전체 document 페이지에 대한 이벤트이므로, 특정 컴포넌트 안에서만 발생된다는 착각은 금물!!

Ref

특정 노드나 컴포넌트에 레퍼런스 값을 만들어 주는 것입니다. 때문에 ref를 통해서 노드나 리액트 요소에 접근하여 값을 얻어낼 수 있습니다.

  • class형 컴포넌트 : createRef
  • 함수형 컴포넌트 : useRef

함수형 컴포넌트에서 useRef를 사용하려면 import별도의 호출을 해주어야 한다.

import React, { useEffect, useRef } from 'react';

export default function Header() {
  const modalRef = useRef();
  
   return (
    <div className="ModalPopup" ref={modalRef}>
       ...
    </div>
   );
}

아이디어 📚

  1. 문서 전체에 이벤트 리스너를 붙인다.
  • modal 객체 외부를 클릭하면 modal이 꺼져야 하기 때문이다.
  • 이벤트 리스너의 종류는 mousedown
    💡 mousedown : 클릭하는 순간 이벤트가 동작된다.
    💡 click : 클릭이 끝나는 순간 이벤트가 동작한다.
  1. outside 클릭의 기준이 되는 HTML 요소를 잡아준다.
  • React ref 이용
  1. 클릭 이벤트 mousedown가 발생시, event.targetref에 저장된 요소를 포함하는지 확인한다.
  1. 포함이 되지 않는다면, outside 클릭이 되는 것이다.

구현한 코드 🙌

import React, { useEffect, useRef } from 'react';
import './Header.scss';

export default function Header({
  btnClick,
  btnName,
  isClickFilterBtn,
  children,
}) {
  const modalRef = useRef();

  useEffect(() => {
    document.addEventListener('mousedown', clickModalOutside);

    return () => {
      document.removeEventListener('mousedown', clickModalOutside);
    };
  });

  const clickModalOutside = event => {
    if (btnClick && !modalRef.current.contains(event.target)) {
      isClickFilterBtn();
    }
  };

  return (
    <div className="ModalPopup" ref={modalRef}>
      <header className="modalHeader">
        <span className="modalReset">
          <i className="fas fa-redo" />
          &nbsp;초기화
        </span>
        <h1 className="modalName">{btnName}</h1>
        <span className="modalClose" onClick={isClickFilterBtn}>
          X
        </span>
      </header>
      {children}
    </div>
  );
}
  1. useRef 사용을 위해 import별도의 호출을 해주었다.
import React, { useEffect, useRef } from 'react';

const modalRef = useRef();
  1. 모달창 자체의 큰 틀은 <div className="ModalPopup">이다. 클릭의 기준이 될 요소 자체이므로, 여기에 ref={modalRef}를 달았다.
  2. 리렌더링 될 때마다 반응하는 useEffect에 사용자의 클릭에 반응할 document.addEventListener를 달아주었다. 사용자의 클릭 mousedown이 발생할 때마다 clickModalOutside() 메서드가 호출된다.
  3. 또한 document.addEventListener는 스스로 종료되지 않는다. 만약 제거해주지 않으면, 전에 실행되었던 이벤트가 이번에 또 실행되고, 그 다음에 또 실행되는 이벤트가 축적된다. 따라서 useEffect에 언마운트 (=cleanup 함수)처리를 해주었고, document.removeEventListener로 이벤트리스너를 종료하였다.

💡 축적된 이벤트들이 완전히 제거되지는 않는다는 것을 console.log로 찍어보았다. 언마운트 처리를 해주지 않았을 때보다는 축적된 이벤트들이 확실히 적었다.

useEffect 에서는 함수를 반환할 수 있는데, 이를 cleanup 함수라고 부릅니다. cleanup 함수는 useEffect에 대한 뒷정리를 해준다고 이해하시면 되는데요, 컴포넌트가 사라질 때 cleanup 함수가 호출됩니다.

  1. clickModalOutside 메서드는 클릭 이벤트가 발생될 때마다 실행되는 메서드이다. 클릭 이벤트가 발생되면 모달창이 꺼지는 로직을 처리해주었다.
 const clickModalOutside = event => {
    if (btnClick && !modalRef.current.contains(event.target)) {
      isClickFilterBtn();
    }
  };
  • 클릭시의 event 인자를 가지고 온다.
  • btnClick은 모달창이 켜지면 true, 꺼지면 false가 되는 state 값이다. 따라서 모달창이 true일 때만 성립된다.
  • 사용자가 클릭한 영역이 모달창에 속한 요소이면 false가 되므로 조건문이 성립하지 않는다. 따라서 모달창이 꺼지지 않는다.
  • 사용자가 클릭한 영역이 모달창에 속해 있지 않으면 true가 되므로 조건문이 성립한다. 따라서 모달창이 꺼진다.
profile
FE Developer

1개의 댓글