1. Overlay 의 재설계

    <aside> 💪

    화면을 띄우는 모달 방식을 검토해 보자

    </aside>

    tooltip, modal 등 화면 위에서 보여지는 것들의 구현 방식이 일관되지 않아 프로젝트 내 일관성, 유지보수성을 고려해 오버레이를 재설계함.

    [ 적용 근거 ]

    [ 적용 결과 ]

  2. 라이브러리 설치

pnpm install overlay-kit
  1. 현재 프로젝트 내 재설계 대상
  2. 기존 ModalWrapper 의 구현 방식

const ModalWrapper = forwardRef<ModalWrapperRef, ModalWrapperProps>(function Modal(
	{ children, backdrop = false },
	ref,
) {

	// 오버레이의 열림/닫힘 상태를 관리하는 state
	const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
	const dialog = useRef<HTMLDialogElement>(null);

	// 오버레이를 여닫는 함수
	useImperativeHandle(ref, () => ({
		open() {
			dialog.current?.showModal();
			setIsModalOpen(true);
		},
		close() {
			dialog.current?.close();
			setIsModalOpen(false);
		},
	}));
	const modalElement = document.getElementById('modal');

	// 오버레이 외부 영역 클릭 시의 동작
	const handleClick = (e: MouseEvent<HTMLDialogElement>) => {
		if (e.target === dialog.current) {
			dialog.current?.close();
			setIsModalOpen(false);
		}
	};

	if (!modalElement) {
		return null;
	}

	// 오보레이 열렸을 때만 콘텐츠 표시되도록 설정
	const content =
		typeof children === 'function'
			? (children as (props: { isModalOpen: boolean }) => ReactNode)({ isModalOpen })
			: children;

	return createPortal(
		<dialog
			ref={dialog}
			onClick={handleClick}
			className={`custom-dialog bg-transparent ${backdrop ? 'with-backdrop' : ''}`}
		>
			{content}
		</dialog>,
		modalElement,
	);
});

export default ModalWrapper;

  1. root 위치에 OverlayProvider 추가
const App = () => {
	return (
		<QueryClientProvider client={queryClient}>
			<div style={{ fontSize: '16px' }}>
				<ReactQueryDevtools initialIsOpen={false} />
			</div>
			<OverlayProvider>
				<Provider>
					<RouterProvider router={router} />
				</Provider>
			</OverlayProvider>
		</QueryClientProvider>
	);
};

OverlayProvideroverlay-kit의 핵심 상태 관리 컨텍스트를 제공하여, 앱 전역에서 모달과 오버레이 컴포넌트의 열림/닫힘 상태를 일관되게 관리하기 위해 추가했습니다. 이를 통해 여러 곳에서 호출되는 모달이 충돌 없이 정상적으로 작동하도록 돕습니다.

  1. Overlay 컴포넌트 적용
import { overlay } from 'overlay-kit';
import { ReactNode } from 'react';

interface OverlayProps {
	backdrop?: boolean;
	content: (props: { isOpen: boolean; close: () => void }) => ReactNode;
}

// Overlay 컴포넌트 구현
export const Overlay = ({ content, backdrop = true }: OverlayProps) => {
	// overlay.open: 모달 열기 함수 (overlay-kit 제공)
	overlay.open(({ isOpen, close }) =>
		isOpen ? (
			<div
				className={`fixed inset-0 z-50 flex items-center justify-center ${
					backdrop ? 'bg-[rgba(0,0,0,0.7)]' : 'bg-transparent'
				}`}
				// 바깥 영역 클릭 시 모달 닫기
				onClick={close}
			>
				{/* 모달 내용 컨테이너,  내부 클릭 시 바깥(onClick)으로 이벤트 전파되지 않도록 차단 */}
				<div onClick={(e) => e.stopPropagation()}>
					{/* 사용자에게 전달된 content 함수 실행, isOpen/close 전달 */}
					{content({ isOpen, close })}
				</div>
			</div>
		) : null, // 모달이 닫혔을 경우 렌더링하지 않음
	);
};

  1. Overlay 사용법
 Overlay({
  backdrop: false, // 배경을 투명하게 표시하고 싶을 때
  content: ({ isOpen, close }) => ( <~~Modal isOpen={isOpen} isClose={close} />,) //  isOpen, close는 필요한 값만 사용
});

  1. ModalWrapper -> Overlay 교체 작업

(기존)

	const handleCloseDeleteAccountModal = () => {
		deleteAccountModalRef.current?.close();
	};

	const handleOpenDeleteAccountModal = () => {
		deleteAccountModalRef.current?.open();
	};

	<ModalWrapper ref={deleteAccountModalRef} backdrop>
		{(_) => (
			<ModalContentsAlert.DeleteAccount
					onConfirm={handleDeleteAccount}
					onCloseModal={handleCloseDeleteAccountModal}
					userEmail={props.email}
				/>
			)}
	</ModalWrapper>

(교체)


	const handleLogoutModal = () => {
		Overlay({
			backdrop: true,
			content: ({ close }) => (
				<ModalContentsAlert.Logout onConfirm={reloginWithoutLogout} onCloseModal={close} userEmail={props.email} />
			),
		});
	};