파괴적인 동작을 할 때 사용할 confirm modal 만들기

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

interface ConfirmModalProps {
  isModalOpen: boolean;
  content: string;
  handleSubmit: VoidFunction;
  handleClose: VoidFunction;
}

const ConfirmModal = ({
  isModalOpen,
  content,
  handleSubmit,
  handleClose,
}: ConfirmModalProps) => {
  if (!isModalOpen) return null;

  const ref = React.useRef<HTMLDivElement>(null);
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (ref.current && !ref.current.contains(target)) {
        handleClose();
      }
    };

    document.addEventListener("click", handleClickOutside, true);
    return () => {
      document.removeEventListener("click", handleClickOutside, true);
    };
  }, []);

  return ReactDOM.createPortal(
    <div className="fixed bg-black bg-opacity-60 z-50 p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
      <div
        ref={ref}
        className="relative mx-auto border border-solid border-gray-200 rounded-lg w-full h-full max-w-md md:h-auto"
      >
        <div className="relative bg-white rounded-lg shadows">
          <button
            type="button"
            className="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
          >
            <div onClick={handleClose} className="w-6 h-6">
              <Xmark />
            </div>
            <span className="sr-only">Close modal</span>
          </button>
          <div className="p-6 text-center">
            <AlertIcon />
            <h3 className="mb-5 text-lg font-normal text-gray-500">
              {content}
            </h3>
            <div className="flex gap-2 justify-center">
              <Button onClick={handleSubmit} style_type="alert" type="button">
                확인
              </Button>
              <Button
                onClick={handleClose}
                style_type="secondary"
                type="button"
              >
                취소
              </Button>
            </div>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
};

export default ConfirmModal;

디자인은 단순한 tailwind 템플릿을 사용했다.

 

중요한 부분은 Portal이었는데 주로 modal 컴포넌트에서 사용되며 쉽게 이해하면 다른 dom 노드에 하위요소로 추가 시켜줄 수 있다.

 

위 코드에서는 body 태그의 하위컴포넌트로 추가하게 되며 실제 렌더링 되는지 확인해보면 다음과 같이 렌더링 된다

 

이후 아래와 같이 불러온다.

 

     <>
        <li onClick={setTodoOpen} className="py-4 px-5">
          <div className="relative px-0 py-2 flex justify-between items-center w-full text-base text-gray-800 text-left bg-white border-0 rounded-none focus:outline-none">
            {title}
            <TodoMenu
              handleUpdateMode={handleUpdateMode}
              handleModalOpen={handleModalOpen}
            />
          </div>
          <AnimatePresence mode="wait">
            {isTodoOpen() ? (
              <motion.div {...openAnimation}>{content}</motion.div>
            ) : null}
          </AnimatePresence>
        </li>
        <ConfirmModal
          content="TODO를 삭제하시겠습니까?"
          isModalOpen={isModalOpen}
          handleSubmit={handleDeleteClick}
          handleClose={handleModalClose}
        />
      </>

 

문제가 있다면 이렇게 붙여도 될까? 하는 부분이다. 처음 코드를 볼 때 Todo 컴포넌튼데 왠 modal이 붙어있고 상태로 관리되고 있다.

 

만약 view와 logic으로 분리된다면 조금 더 명확하게 구분할 수 있지 않을까? 생각이 들어 분리하려고 한다.

 

회고

오늘 공부한 걸 다 적으려니 코드적인 부분이 많았고 기능적으로 정확하게 글로 정리할 수 없을거 같았다.

그래서 간단하라도 적는게 좋을거 같아 짧게라도 글을 작성했다.

복사했습니다!