"use client";
import { useEffect, useRef } from "react";
import { usePreservedCallback } from "./usePreservedCallback";
import { useRouter } from "next/navigation";
/**
* 커스텀 모달을 띄우지 않고 chrome 경고창을 띄우려면 callback에서
* e.returnValue = ""; 세팅이 필요합니다.
*/
export function useBeforeUnload(
isDirty: boolean,
callback: (e?: BeforeUnloadEvent) => Promise<boolean>,
/** 브라우저 뒤로가기에서 pushState로 뒤로갈 때 가야할 이전페이지 주소 */
prevUrl?: string
) {
/**
* 새로고침/탭 닫기 방지 (브라우저 기본 팝업)
* 브라우저 기본 팝업은 막을 수가 없어서 callback 사용 안함.
*/
const beforeUnloadCallback = usePreservedCallback(callback);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (!isDirty) return;
e.preventDefault();
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [isDirty, beforeUnloadCallback]);
/**
* 브라우저 뒤로가기 방지
*/
const blockingRef = useRef(false);
const isProgrammaticNavigationRef = useRef(false);
const currentUrlRef = useRef<string | null>(null);
const router = useRouter();
useEffect(() => {
// 현재 URL 저장
currentUrlRef.current = window.location.href;
if (!currentUrlRef.current) return;
// isDirty가 true일 때만 history entry 추가
if (isDirty) {
// 현재 페이지를 history에 추가하여 뒤로가기 감지 가능하게 함
window.history.pushState({ guarded: true }, "", window.location.href);
// 추가: 브라우저 뒤로가기 버튼을 감지하기 위한 더미 엔트리 추가
window.history.pushState({ dummy: true }, "", window.location.href);
}
const onPopState = async (e: PopStateEvent) => {
// 프로그래밍 방식 네비게이션은 무시
if (isProgrammaticNavigationRef.current) {
isProgrammaticNavigationRef.current = false;
currentUrlRef.current = window.location.href;
return;
}
// 더미 엔트리에서 뒤로가기한 경우 (실제 뒤로가기 감지)
if (e.state && e.state.dummy) {
// 더미 엔트리를 제거하고 모달 표시
window.history.replaceState(null, "", window.location.href);
if (!isDirty) {
currentUrlRef.current = window.location.href;
return;
}
if (blockingRef.current) return;
// 모달 표시 로직으로 진행
} else if (!isDirty) {
// isDirty가 false면 URL 업데이트하고 진행
currentUrlRef.current = window.location.href;
return;
} else if (blockingRef.current) {
return;
}
blockingRef.current = true;
// 모달 표시
const ok = await beforeUnloadCallback();
if (ok) {
// 사용자가 확인했으면 뒤로가기 실행
isProgrammaticNavigationRef.current = true; // 프로그래밍 방식으로 표시
// 가장 확실한 방법: 직접 URL 변경
if (prevUrl) {
// Next.js router를 사용해서 네비게이션
router.push(prevUrl);
} else {
// prevUrl이 없으면 history에서 이전 URL 찾기
// 방법 1: Next.js router 사용
try {
router.back();
} catch (error) {
console.error("router.back() failed:", error);
// 방법 2: window.history.back() 사용
try {
window.history.back();
} catch (error2) {
console.error("window.history.back() failed:", error2);
// 방법 3: 기본 페이지로 이동
router.push("/");
}
}
}
// URL 업데이트
setTimeout(() => {
currentUrlRef.current = window.location.href;
}, 100);
} else {
// 사용자가 취소했으면 현재 페이지로 되돌리기
window.history.pushState(null, "", currentUrlRef.current);
}
setTimeout(() => {
blockingRef.current = false;
}, 100);
};
window.addEventListener("popstate", onPopState);
return () => {
window.removeEventListener("popstate", onPopState);
};
}, [isDirty, beforeUnloadCallback, prevUrl, router]);
// Proxy를 사용해서 router의 모든 메서드를 가로채서 beforeunload 체크를 적용
const guardedRouter = new Proxy(router, {
get(target, prop) {
const originalMethod = target[prop as keyof typeof target];
// 네비게이션 관련 메서드들만 가로채기
if (
typeof originalMethod === "function" &&
["push", "replace", "back", "forward", "refresh"].includes(
prop as string
)
) {
return async (...args: any[]) => {
if (isDirty) {
const ok = await beforeUnloadCallback();
if (!ok) return;
}
// 프로그래밍 방식 네비게이션임을 표시
isProgrammaticNavigationRef.current = true;
// back() 메서드인 경우 prevUrl 사용
if (prop === "back" && prevUrl) {
const result = (target.push as any).apply(target, [prevUrl]);
// URL 업데이트
setTimeout(() => {
currentUrlRef.current = window.location.href;
}, 0);
return result;
}
const result = (originalMethod as any).apply(target, args);
// URL 업데이트
setTimeout(() => {
currentUrlRef.current = window.location.href;
}, 0);
return result;
};
}
// 다른 속성들은 그대로 반환
return originalMethod;
},
});
return {
guardedRouter,
};
}
JavaScript
복사