밝을희 클태

[ React ] Custom Confirm Modal 만들기 본문

KEYNUT 프로젝트

[ React ] Custom Confirm Modal 만들기

huipark 2024. 8. 24. 15:34

 프로젝트에서 기본 Confirm Modal을 사용하고 있었는데 일단 못생기기도 했고 모바일에서 대화창 숨기기랑 항목이 데스크톱과 다르게 있는데 이 항목을 한번 클릭하면 더 이상 Confirm Modal이 뜨지 않는다. 우리 프로젝트에서 Confirm Modal의 입력을 꼭 "예" 를 받아다 다음 상황으로 진행이 되기 때문에 실수로 대화창 숨기기를 클릭해 버리면 사용자는 왜 다음 상황으로 안 넘어가는지 알 수 없는 문제가 발생했다.

 

그래서 useContext API를 사용해 전역에서 사용가능한 Confirm Modal을 만들어 볼거다.

 

ModalProvider.jsx

  형태 용도
modalMessage String 모달의 메인 메세지
modalSubMessage String 모달의 서브 메세지
isSelectableModal boolean 선택 가능한 모달 여부
resolvePromise function 'Promise'를 완료 하는 함수

 

'use client';

import { usePathname } from 'next/navigation';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';

const ModalContext = createContext();

export const useModal = () => useContext(ModalContext);

export const ModalProvider = ({ children }) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [modalMessage, setModalMessage] = useState('');
  const [modalSubMessage, setModalSubMessage] = useState('');
  const [isSelectableModal, setIsSelectableModal] = useState(false);
  const [size, setSize] = useState('w-72');
  const [resolvePromise, setResolvePromise] = useState(null);
  const pathName = usePathname();

  const openModal = useCallback(({ message, subMessage = '', isSelectable = false, size }) => {
    setModalMessage(message);
    setModalSubMessage(subMessage);
    setIsSelectableModal(isSelectable);
    setIsModalOpen(true);
    if (size) setSize(size);

    return new Promise(resolve => {
      setResolvePromise(() => resolve);
    });
  }, []);

  const resetModalState = useCallback(() => {
    setModalMessage('');
    setModalSubMessage('');
    setIsSelectableModal(false);
    setSize('w-72');
  }, []);

  const closeModal = useCallback(() => {
    if (resolvePromise) {
      resolvePromise(false);
      setResolvePromise(null);
    }
    setIsModalOpen(false);
    resetModalState();
  }, [resolvePromise]);

  const confirmModal = useCallback(() => {
    if (resolvePromise) {
      resolvePromise(true);
      setResolvePromise(null);
    }
    setIsModalOpen(false);
    resetModalState();
  }, [resolvePromise]);

  useEffect(() => closeModal(), [pathName]);

  return (
    <ModalContext.Provider
      value={{
        isModalOpen,
        modalMessage,
        modalSubMessage,
        openModal,
        closeModal,
        confirmModal,
        isSelectableModal,
        size,
      }}
    >
      {children}
    </ModalContext.Provider>
  );
};

Modal.jsx

'use client';

import { useModal } from './ModalProvider';

export default function Modal() {
  const { isModalOpen, modalMessage, modalSubMessage, closeModal, confirmModal, isSelectableModal, size } = useModal();

  if (!isModalOpen) return null;

  return (
    <div
      className="fixed top-0 left-0 w-screen custom-dvh flex justify-center items-center bg-black bg-opacity-50 z-50"
      onClick={e => {
        if (e.currentTarget === e.target) closeModal();
      }}
    >
      <div
        className={`${size} rounded space-y-4 p-3 bg-white flex flex-col justify-center items-center border-solid border`}
      >
        <div className="space-y-2">
          <p className="font-semibold text-lg text-center break-all whitespace-pre-line">{modalMessage}</p>
          {modalSubMessage && (
            <p className="text-center text-gray-400 text-sm whitespace-pre-line">{modalSubMessage}</p>
          )}
        </div>
        <div className="flex justify-center space-x-2 h-10 font-semibold">
          <button
            className={`flex rounded justify-center items-center w-32 ${
              isSelectableModal ? 'bg-gray-200' : 'bg-black text-white'
            } `}
            onClick={closeModal}
          >
            {isSelectableModal ? '취소' : '확인'}
          </button>
          {isSelectableModal && (
            <button
              className="flex rounded justify-center items-center w-32 bg-black text-white"
              onClick={confirmModal}
            >
              확인
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

 

실제 사용 예시

아래와 같이 ModalProvierModalLayout 컴포넌트에 추가해준다.

export default function MainLayout({ children }) {
  return (
    <ModalProvider>
      {children}
      <Modal />
    </ModalProvider>
  );
}
import { useModal } from '@/app/(main)/_components/ModalProvider';

export default function Page() {
  const { openModal } = useModal();
      
  const openConfirmModal = async () => {
    const res = await openModal({
      message: 'hello world?',
      isSelectable: true,
    });
    if (!res) return;

    // 추가 작업이 여기에 위치
  };
      
  return <button onClick={openConfirmModal}>모달 열기</button>;
}