유명한 담벼락

모달 바깥 부분이나 ESC 키를 누르면 모달이 닫히는 기능 구현

by 담담이담

모달 내부의 닫기 버튼을 클릭하지 않아도 

모달이 닫히도록 구현해봤다.

 

 

 

일단 먼저 

import { ReactNode, useEffect, useRef, useState, MouseEvent } from "react";
import { createPortal } from "react-dom";
import styles from "./ModalWrapper.module.css";
import useNotScroll from "@/hooks/useNotScroll";

interface ModalWrapperProp {
  children: ReactNode;
  size: "lg" | "md" | "sm";
  handleModalClose: () => void;
}

/** size는 모달의 크기만 구분하기 때문에 세부적인 건 다른 곳에서 처리해야한다 */
const ModalWrapper = ({ children, size, handleModalClose }: ModalWrapperProp) => {
  const portalRoot = document.getElementById("modal-root") as HTMLElement;

  const modalOutsideRef = useRef<HTMLDivElement>(null);

  // 모달 바깥 부분 클릭 시 모달 닫히는 기능
  const modalOutsideClick = (e: MouseEvent) => {
    if (modalOutsideRef.current === e.target) {
      handleModalClose();
    }
  };

  // esc 키 누르면 모달 닫히는 기능
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        handleModalClose();
      }
    };

    // 이벤트 리스너 등록
    document.addEventListener("keydown", handleEscape);

    // 컴포넌트가 언마운트될 때 이벤트 리스너 제거
    return () => {
      document.removeEventListener("keydown", handleEscape);
    };
  }, [handleModalClose]);

  return createPortal(
    <div onClick={modalOutsideClick} ref={modalOutsideRef} className={styles.root}>
      <div className={styles[size]}>{children}</div>
    </div>,
    portalRoot
  );
};

export default ModalWrapper;

 

모달은 createPortal을 이용해서 만들었고, 

모든 모달을 children으로 받아서 ModalWrapper의 CreatePortal로 감쌌다.

 

이 때, 프롭으로 모달 뿐만 아니라 handleModalClose라는 

모달 닫기 함수도 받아서 구현했다.

 

  const modalOutsideRef = useRef<HTMLDivElement>(null);

  // 모달 바깥 부분 클릭 시 모달 닫히는 기능
  const modalOutsideClick = (e: MouseEvent) => {
    if (modalOutsideRef.current === e.target) {
      handleModalClose();
    }
  }

 

useRef를 이용해서 모달 바깥 부분을 modalOutsideRef로 걸어주었고,

modalOutsideClick이라는 함수를 이용해서 

바깥 부분을 클릭했을 때 모달이 닫히게 만들어줬다.

 

`e.target`과 `e.currentTarget`은 

 이벤트가 어디에서 발생했는지에 대한 정보를 제공하는 속성이다.


- `e.target`: 이벤트가 실제로 발생한 요소(element)를 가리킨다.
- `e.currentTarget`: 이벤트 핸들러가 연결된 요소를 가리킨다.


`e.target`은 실제 클릭한 요소를 가리키므로, 

만약 클릭한 요소가 `modalOutsideRef`와 같다면 클릭한 위치가 모달 바깥 부분이라고 판단할 수 있다.

따라서 `handleModalClose` 함수를 호출하여 모달을 닫을 수 있다.

`e.currentTarget`은 항상 이벤트 핸들러가 연결된 요소를 가리키므로,

클릭한 위치와 상관없이 항상 `modalOutsideRef`를 가리킨다.

 

  // esc 키 누르면 모달 닫히는 기능
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        handleModalClose();
      }
    };

    // 이벤트 리스너 등록
    document.addEventListener("keydown", handleEscape);

    // 컴포넌트가 언마운트될 때 이벤트 리스너 제거
    return () => {
      document.removeEventListener("keydown", handleEscape);
    };
  }, [handleModalClose]);

 

 

디펜던시 배열에 포함된 값이 변경될 때마다 `useEffect` 효과가 실행된다.

`handleModalClose`를 디펜던시 배열에 넣어준 이유는, 

해당 이벤트 핸들러가 모달이 열릴 때 등록되어야 하고, 

모달이 닫힐 때 제거되어야 하기 때문이다.

 

만약 `handleModalClose`가 디펜던시 배열에 없다면, 

컴포넌트가 처음 렌더링될 때 이벤트 핸들러를 등록하고, 

모달이 닫힐 때 제거하는 로직이 한 번만 실행된다.

`useEffect`의 디펜던시 배열에 `handleModalClose`를 포함시킴으로써, 

`handleModalClose`가 변경될 때마다

 `useEffect`가 실행되어 해당 로직이 반복적으로 동작하도록 보장한다.

이로써 모달이 열릴 때마다 새로운 `handleModalClose` 함수를 사용하게 되고, 

모달이 닫힐 때마다 기존에 등록된 이벤트 리스너가 정확히 제거된다.

블로그의 정보

유명한 담벼락

담담이담

활동하기