[OutsideClick] outside click 구현 #1

이말감·2022년 8월 14일
0

!Library

목록 보기
4/5

보통 모달창을 살펴보면 모달의 바깥을 클릭하면 창이 닫히는 모습을 확인할 수 있다.
이 기능을 구현해보자.

아래 링크의 글을 이해하기 위해 작성된 글입니다.

버튼을 이용하여 간단하게 구현

function ClickOutside() {
  const [number, setNumber] = useState(0);

  const handleClickBtn = () => {
    setNumber((prev) => prev + 1);
  };
  return (
    <div className={styles.clickOutside}>
      <div className={styles.out}>
        out
        <button type='button' className={styles.in} onClick={handleClickBtn}>
          number is {number}
        </button>
      </div>
    </div>
  );
}

먼저 아주아주아주 간단하게 버튼을 클릭하면 수가 증가하도록 했다.
이때 녹색 버튼의 바깥 부분을 클릭하면 수가 0이 되도록 구현해보자.

ref 사용하기

ref란?

ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공한다.
그러니까 DOM에 직접 접근할 때 사용한다.
보통 js 에서는 DOM에 접근하려면 document.queryselectordocument.getelementby~ 를 이용한다. 하지만 리액트에서는 ref를 사용하면 된다!

ref를 사용하기 위해선 useRef를 사용하면 된다.

// 커스텀 훅 useOutsideClick의 ref
const ref = useRef<HTMLButtonElement>(null);
// 커스텀 훅 useOutsideClick로부터 반환받은 ref
const ref = useOutsideClick(handleOutsideClick);

그리고 버튼에 ref를 추가한다.

// 커스텀 훅 useOutsideClick로부터 반환받은 ref이 들어간다.
<button type='button' className={styles.in} onClick={handleClickBtn} ref={ref}>
          number is {number}
</button>

여기서 잠깐!
나는 타입스크립트를 이용해서 코드를 작성하고 있기 때문에, 기존 코드와 같이

const ref = useRef();

이렇게 작성하면 아래와 같은 오류가 발생한다.

받아야 하는 형식은 LegacyRef<HTMLButtonElement> | undefined인데 들어가는 형식이 MutableRefObject<undefined>이기 때문에 오류가 발생했다.

이 문제는..도대체 무엇이 문제일까 !!!!!!!
정답은 useRef의 초기값을 null로 설정해주는 것이다.

const ref = useRef<HTMLButtonElement>(null);

이를 적용하면 감쪽같이 에러가 사라진다.
아래의 블로그를 참고하여 해결할 수 있었다. 이 내용에 대해 따로 공부해야 할 필요가 있다 !

한 고비를 넘겼더니 또 다른 에러가 나를 반기고 있었다.

타입이 맞지 않는다는 에러가 발생했다.
MouseEvent로 타입을 설정해줬는데 왜! 안되는 걸까

MouseEvent는 아래와 같이 target을 가지고 있지 않기 때문에 에러가 발생했다.

그렇다면 MouseEvent가 상속받은 타입들을 하나씩 거슬러 올라가보자.

UIEvent에도, SyntheticEvent에도 target이 없다.

최상단인 BaseSyntheticEvent에 target이 있는 것을 확인할 수 있다!

그렇다면 타입을 어떻게 설정해줘야 할까?

먼저 클릭 이벤트이기 때문에 addEventListener에 MouseEvent라 적혀있다. 그러므로 MouseEvent를 설정해줘야 하고, 위에서 알아냈듯이 BaseSyntheticEvent도 설정해주면 된다.

const handleClick = (event: BaseSyntheticEvent | MouseEvent) => {

이렇게 설정해주면 더이상 에러가 발생하지 않는다.

참고

// custom hook
 const useOutsideClick = (callback: () => void) => {
    // button에 ref를 걸었음
    const ref = useRef<HTMLButtonElement>(null);

    useEffect(() => {
      const handleClick = (event: BaseSyntheticEvent | MouseEvent) => {
        callback();
      };

      document.addEventListener('click', handleClick);

      return () => {
        document.removeEventListener('click', handleClick);
      };
    }, [callback, ref]);

    return ref;
  };

현재는 조건이 설정되어 있지 않기 때문에 버튼을 클릭하면 callback()이 실행되어 수가 증가하지 않는다.(계속 0)

그렇다면 조건을 설정해보자.

// ref가 존재하고, ref 안에 클릭한 부분이 없을 때만 동작한다.
if (ref.current && !ref.current.contains(event.target)) {
	callback();
}

ref.current가 참이라는 말은 ref가 존재한다는 뜻이고,
!ref.current.contains(event.target)가 참이라는 말은 클릭한 부분이 ref에 속하지 않는다는 말이다.
그러니까 ref가 존재하면서 ref의 바깥쪽을 눌렀을 때만 조건이 성립하여 callback()이 동작한다는 것을 뜻한다.

ref.currentevent.target이 정확하게 무엇인지 확인하기 위해 콘솔을 찍어봤다.

첫 번째가 ref.current이고, 우리가 ref로 설정한 버튼이 콘솔에 찍힌다.
그리고 두 번째는 event.target이고 현재 우리가 클릭한 바깥쪽 div가 콘솔에 찍히는 모습을 확인할 수 있다.

이 과정을 거치면 아래와 같이 버튼을 클릭하면 수가 증가하고, 버튼의 바깥쪽을 클릭하면 수가 0이 되는 모습을 확인할 수 있다.

document.addEventListener('click', handleClick);

return () => {
	document.removeEventListener('click', handleClick);
};

그리고 컴포넌트 렌더링이 완료되면 이벤트가 등록되고, 컴포넌트가 제거되면 이벤트가 제거된다.

전체 코드

style은 따로 적용해야함

import { BaseSyntheticEvent, useEffect, useRef, useState } from 'react';
import styles from './clickOutside.module.scss';

function ClickOutside() {
  // count number
  const [number, setNumber] = useState(0);

  // 버튼 클릭 이벤트
  const handleClickBtn = () => {
    setNumber((prev) => prev + 1);
  };

  // 바깥쪽 클릭 이벤트
  const handleOutsideClick = () => {
    setNumber(0);
  };

  // custom hook
  const useOutsideClick = (callback: () => void) => {
    // button에 ref를 걸었음
    const ref = useRef<HTMLButtonElement>(null);

    useEffect(() => {
      const handleClick = (event: BaseSyntheticEvent | MouseEvent) => {
        // ref가 존재하고, ref 안에 클릭한 부분이 없을 때만 동작한다.
        if (ref.current && !ref.current.contains(event.target)) {
          callback();
        }
      };

      document.addEventListener('click', handleClick);

      return () => {
        document.removeEventListener('click', handleClick);
      };
    }, [callback, ref]);

    return ref;
  };

  const ref = useOutsideClick(handleOutsideClick);

  return (
    <div className={styles.clickOutside}>
      <div className={styles.out}>
        out
        <button type='button' className={styles.in} onClick={handleClickBtn} ref={ref}>
          number is {number}
        </button>
      </div>
    </div>
  );
}

export default ClickOutside;
profile
전 척척학사지만 말하는 감자에요

0개의 댓글