치춘짱베리굿나이스

useClickOutside 직접 구현하기 본문

ClientSide/React

useClickOutside 직접 구현하기

치춘 2022. 7. 1. 19:41

useClickOutside

모달을 라이브러리를 쓰지 않고 직접 구현하면서 (react-portal을 사용하였다) 모달 바깥을 클릭했을 때 모달이 닫히도록 구현을 하고 싶었다

유용한 훅을 많이 포함하고 있는 패키지인 react-use를 설치하여 useClickAway 훅을 이용하면 한번에 해결되겠지만, 지금 프로젝트에서 react-use가 설치되어 있지 않기도 했고 (이거 하나 때문에 설치하기도 애매하고) 생각보다 구현이 어렵지 않아 훅을 직접 제작해 보았다

코드

import { useEffect, useRef } from 'react';

const useClickOutside = onClicKOutside => {
  const ref = useRef(null);

  const handleClickOutside = (event) => {
    if (ref.current && !ref.current.contains(event.target)) onClicKOutside();
  };

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

  return { ref };
};

export default useClickOutside;

핸들러에 의해 실행되는 함수는 외부에서 받아올 수 있도록 하였다

이 함수는 핸들러 내부에서 조건문으로 한번 감싸지는데, 이 조건문이 어떤 역할을 하는지는 아래에 적어두었다

로직

ref으로 목적 요소 지정하기

const CharSelectModal = ({ userInfo, handleClickChar, handleClickClose }) => {
  const { ref: modalRef } = useClickOutside(handleClickClose);

  return (
    <ModalPortal>
      <div className="modal-background">
        <div className="char-select-modal-container" ref={modalRef}>
        ...

목적 요소의 ref attribute에 useClickOutside 내부에서 useRef을 통해 초기화한 ref를 넣는다

만약 모달 컴포넌트의 바깥을 눌렀을 때 핸들러를 걸고 싶다면 모달 컴포넌트의 ref를 지정하면 된다

useEffect로 이벤트 걸기

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

document.addEventListener에서 document는 최상위 요소, 나아가 페이지 (html 문서) 전체를 가리킨다

(document.querySelector() 가 페이지 내의 모든 요소들 중 하나를 선택하는 메서드 함수라는 것을 기억하자)

useEffect를 통해 컴포넌트 마운트 시, 또는 ref에 변화가 있을 시 페이지 전체에 이벤트 리스너를 걸어주고, 페이지의 종류가 ‘mousedown’ 이므로 페이지 어디를 클릭해도 이벤트가 발생한다

또한, 컴포넌트 언마운트 시에는 이벤트 리스너를 제거한다

우리는 handleClickOutside라는 이벤트 핸들러를 리스너로 페이지 전역에 걸어줄 것이다

이벤트 핸들러 선언

const handleClickOutside = (event) => {
    if (ref.current && !ref.current.contains(event.target)) onClicKOutside();
};

이벤트 발생 시마다 실행될 핸들러를 선언한다

핸들러는 인자로 event (타입은 MouseEvent 이므로, 타입스크립트에서 이부분을 추가해주면 된다) 를 받는다

핸들러 내부엔 조건문이 있는데, 이 조건문은 다음과 같은 조건을 만족했을 때 발동된다

  1. ref.current가 존재할 때 (ref가 특정 요소에 제대로 걸려 있을때)
  2. ref.current가 ‘이벤트가 발생한 요소' 를 포함하고 있지 않을 때

handleClickOutside가 발생하는 이벤트 (mousedown) 는 컴포넌트 마운트 시 페이지 전체에 리스너가 걸려 있으므로 페이지 어디를 클릭해도 해당 핸들러가 호출된다

하지만 우리가 원하는 것은 ref로 지정한 요소의 바깥을 클릭했을 때 이벤트가 발동하는 것이므로, ref.current포함되지 않은 (자기자신이거나, 자식이 아닌) 요소를 클릭했을 경우에만 원하는 이벤트가 발동되도록 조건문으로 한번 감싸준 것이다

핸들러의 인자로 event를 받지 않아도 동작한다?!

이전 코드에서 event에 가로줄이 쭉 그어지지만 동작은 잘 하는 것을 볼 수 있었다

event 키워드는 묵시적으로 window 객체의 event 프로퍼티를 가져오므로, 변수를 찾지 못했다는 에러 없이 제대로 동작한다

window 객체는 브라우저에서 동작하는 코드의 전역 객체 (Node.js에서의 Global과 같다) 라고 생각하면 된다

하지만 이 방법은 deprecated 되었으며, 아예 동작을 안 하는 것은 아니지만 공식에서 더이상 권장하는 방법이 아니므로 인자를 제대로 받아 사용하자

결론

useClickOutside는 결국 페이지 전역에 이벤트 리스너를 걸고, 이벤트가 발생할 때마다 발생한 위치가 어느 요소인지 판단하여 ref로 지정한 요소 또는 그 하위 요소가 아닐 때에만 이벤트가 발생하도록 해 준다

react-use를 쓰기엔 내부에서 사용하는 훅이 별로 없어서 패키지를 설치하기엔 애매할 때 직접 구현하는 것도 좋아보이는군

참고자료

Detect click outside React component

'ClientSide > React' 카테고리의 다른 글

React를 클론코딩 #1 가상 돔  (0) 2022.10.05
React  (0) 2022.10.01
react-portal 사용해보기  (0) 2022.05.15
데이터 불러오기, Suspense  (0) 2022.05.13
IntersectionObserver + 무한스크롤  (0) 2022.05.12
Comments