
같은글 노션링크 | 코드 변경 전후를 명확하게 보실수있습니다.
1.1 도입이유
1.2 도입 목표
2.1 기존 모달 로직
2.2 context 생성
2.3 모달 사용하기
2.4 ReactDom.CreatePortal
현재 AelrtModalContext 로 모달을 사용하고있다. 기능적으로 문제는 없지만, 해당 코드를 볼수록 디자인과 기능이 강결합돼 있어서 Headless 처럼 두 로직을 분리해야 하지 않을까 라는 생각이 들었다.
또, 다른 UI의 모달을 새로 만든다면, 기존의 것(AlertModalContext)을 재사용할 수 없으므로, XXXModalContext를 여러개 새로 만들어야만 하는 상황이다. 그래서 모달을 잘 만들고 효율적으로 관리할 수 있는 방법을 찾아보게 되었다.
문제점
// context/AlertModalProvider.tsx
export const useAlertModalContext = () => {
  return useContext(AlertModalContext);
};
export const AlertModalContext = createContext({
  showAlert(message: string) {},
});
export const AlertModalProvider = ({ children }: PropsWithChildren) => {
  const [message, setMessage] = useState('');
  const [isOpen, setIsOpen] = useState(false);
  const showAlert = (message: string) => {
    setIsOpen(true);
    setMessage(message);
  };
  const closeAlert = () => {
    setIsOpen(false);
    setMessage('');
  };
  return (
    <AlertModalContext.Provider value={{ showAlert }}>
      {children}
      {isOpen && (
        <Backdrop whiteBoard>
          <div
            css={css`
              display: flex;
              flex-direction: column;
              align-items: center;
              justify-content: space-around;
              width: 85%;
              height: 200px;
              font-size: 22px;
              font-family: 'Noto Sans KR';
            `}
          >
            {message}
            <Button
              onClick={closeAlert}
              css={css`
                width: 90%;
                padding: 5px 0;
                background-color: ${colors.grey200};
                font-size: 20px;
              `}
            >
              닫기
            </Button>
          </div>
        </Backdrop>
      )}
    </AlertModalContext.Provider>
  );
};
context/ModalContext.tsx
export const ModalContext = createContext<TestModals[]>([]);
export const ModalDispatchContext = createContext({
  open: ({ type, props }: TestModals) => {},
  close: () => {},
});
export const useModalContext = () => useContext(ModalContext);
export const useModalDispatchContext = () => useContext(ModalDispatchContext);
function ModalProvider({ children }: { children: ReactNode }) {
  const [openedModals, setOpenedModals] = useState<TestModals[]>([]);
  const open = ({ type, props }: TestModals) => {
    const KEY = Math.random().toString(36).substring(2);
    setOpenedModals(modals => {
      return [
        ...modals,
        {
          type,
          props,
          id: `${type}${KEY}`,
        },
      ];
    });
  };
  const close = () => {
    setOpenedModals(modals => {
      return modals.slice(0, modals.length - 1);
    });
  };
  const dispatch = useMemo(() => ({ open, close }), []);
  return (
    <ModalDispatchContext.Provider value={dispatch}>
      <ModalContext.Provider value={openedModals}>
        {children}
        <CreatePortal />
      </ModalContext.Provider>
    </ModalDispatchContext.Provider>
  );
}
export default ModalProvider;
function RootErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  const { openModal } = useModal();
 ...
if (error.code === 'ERR_NETWORK') {
    openModal({
      type: 'alert', 	// 모달종류
			// 표현할 컨텐츠 전달
      props: { title: '앗...😰', message: ERROR_MESSAGE[998], btnText: '닫기' }, 
    });
    resetErrorBoundary();
    return;
  }
	return (...)
}
모달 종류는 두가지 있다.
const MODAL_COMPONENTS = {
  alert: AlertModal,
  confirm: ConfirmModal,
};
function AlertModal({ onClose, title, message, btnText = '닫기' }: ModalProps) {
  return (
    <BackDrop whiteBoard>
      <div css={alert.alignColumn} role="dialog" tabIndex={0}>
        <h3 css={alert.title}>{title}</h3>
        <p css={alert.message}>{message}</p>
        <Button onClick={onClose} css={alert.button} aria-label={btnText}>
          {btnText}
        </Button>
      </div>
    </BackDrop>
  );
}
function ConfirmModal({  onClose, onSubmit, title, message, btnText,: ModalProps) {
  return (
    <BackDrop whiteBoard>
      <div css={alert.alignColumn}>
        <h3 css={alert.title}>{title}</h3>
        <p css={alert.message}>{message}</p>
        <div css={confirm.btnWrapper}>
          <Button onClick={onSubmit} css={confirm.button}>
            {btnText}
          </Button>
          <Button onClick={onClose} css={confirm.button}>
            닫기
          </Button>
        </div>
      </div>
    </BackDrop>
  );
}
createPortal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식 컴포넌트를 렌더링하는 방법이다. 모달창이 때때로 부모 컴포넌트의 스타일링에 영향받는 경우가 있는데, 이때 모달 컴포넌트가 부모 컴포넌트로부터 독립적인 위치에서 렌더링을 할 수 있게 해준다.
// components/@helper/Modal/CreatePortal.tsx
// 사용가능한 모달종류
const MODAL_COMPONENTS = {
  alert: AlertModal,
  confirm: ConfirmModal,
};
function CreatePortal() {
  const openedModals = useModalContext();
  const { hideModal } = useModal();
  let modalElement;
	// openedModals state를 map으로 순회하면서 
	// 렌더링할 모달컴포넌트를 리턴하고, createPortal에 전달한다.
  const renderModal = openedModals.map(({ type, props, id }: Modals) => {
    const ModalComponent = type && MODAL_COMPONENTS[type];
    const { onSubmit, onClose, ...restProps } = props;
    modalElement = document.getElementById(`${type}-modal`);
    const handleSubmit = async () => {
      if (typeof onSubmit === 'function') await onSubmit();
    };
    const handleClick = () => {
      onClose && onClose();
      hideModal();
    };
    return (
      <ModalComponent
        key={id}
        {...restProps}
        onClose={handleClick}
        onSubmit={handleSubmit}
      />
    );
  });
  return modalElement && createPortal(renderModal, modalElement);
}
export default CreatePortal;

const { showModal } = useModal();
  const confirm = () => {
    showModal({
      type: 'confirm',
      props: {
        title: ' confirm 모달',
        message: '컨펌하시겠습니까?',
        btnText: 'yes',
        onSubmit: () => window.alert('submit'),
      },
    });
  };
  const alert = () => {
    showModal({
      type: 'alert',
      props: {
        title: ' alert 모달',
        message: '알림입니다.',
        btnText: 'yes',
      },
    });
  };
return (
    <>
        <div onClick={confirm}>confirm</div>
        <div onClick={alert}>alert</div>
		</>
  );
참고
낙타 | 효율적인 modal 관리 with React(1)
낙타 | 효율적인 modal 관리 with React(2)
leego | [React] 효율적으로-모달-관리하기 -1
leego | [React] Portal을 사용한 모달창 만들기 -2
[React] 모달 깊게 파헤치기 : 효율적인 모달 관리에 대하여 (useModal custom hooks)
React | Portal을 이용한 Modal 구현
[React] Portal (포탈), Modal 구현하기
https://velog.io/@velopert/react-context-tutorial#context-란
https://mygumi.tistory.com/406
portal, modal 기본적인 사용법
https://abangpa1ace.tistory.com/entry/React-Portal-포탈 - 포탈 설명 굿
https://www.jeong-min.com/33-modal/ - 모달에 추가할만한 기능들