Search

nextjs app router에서 사용 가능한 useBeforeunload

subtitle
Tags
자바스크립트
react
Created
2025/09/19
2 more properties
"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
복사