Next.js 페이지 이탈 방지해보기

전준연·2025년 6월 18일
post-thumbnail

목적

최근 프로젝트를 유지 보수하면서, 페이지 헤더에 있는 Link 태그를 통한 페이지 이동을 방지해야 하는 상황이 생겼다. 기존에도 페이지 이탈을 방지하는 코드가 있었지만, 기존 코드는 새로고침이나 창 닫기만 막을 수 있었고, 내부 라우팅에 대해서는 제대로 동작하지 않았다.

이 문제를 해결하기 위해 여러 블로그를 참고하며 방법을 찾아보았고, 그 내용을 이렇게 블로그로 정리해보았다.

이 글은 Next.js 14의 App Router를 기준으로 작성되었다.

감지해야 하는 동작들

Next.js에서 감지해야 할 페이지 이동은 크게 세 가지로 나눌 수 있다.

  1. 페이지 닫기, 새로고침, a 태그를 통한 이동
  2. router.push() 또는 Link 태그를 통한 내부 라우팅
  3. 브라우저의 뒤로가기/앞으로가기 버튼

그럼 위의 세 가지 경우를 어떻게 감지하고 방지할 수 있는지 하나씩 알아보자.

페이지 닫기, 새로고침, a 태그를 통한 이동

바로 코드를 살펴보자.

useEffect(() => {
  const warningMessage = '작성하던 내용이 모두 사라집니다. 계속하시겠습니까?';

  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
    event.preventDefault();
    event.returnValue = '';
    return warningMessage;
  };

  if (typeof window !== 'undefined') {
    window.addEventListener('beforeunload', handleBeforeUnload);
  }

  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
  };
}, []);

위 코드가 페이지를 닫거나 새로고침하거나, a 태그를 통한 외부 이동이 발생할 때 사용자에게 경고 메시지를 띄워주는 역할을 하는 코드이다. 하나씩 설명해보면

  • warningMessage: 사용자에게 보여줄 경고 문구다. 일부 브라우저에서는 이 메시지를 무시하고 기본 메시지를 보여줄 수 있다.

  • handleBeforeUnload: 브라우저에서 beforeunload 이벤트가 발생할 때 호출된다. event.returnValue에 값을 설정함으로써 사용자에게 페이지를 벗어나려는 의도를 확인하게 만든다.

  • window.addEventListener('beforeunload', ...): 클라이언트 사이드에서만 이벤트를 등록하도록 조건을 추가했다. (typeof window !== 'undefined')

  • return () => { ... }: 컴포넌트가 언마운트될 때 이벤트 리스너를 제거하여 메모리 누수나 중복 등록을 방지한다.

이 코드는 브라우저 기본 동작을 활용하는 방식이라 Next.js에 특화된 기능은 아니지만, 가장 기본적이면서도 확실하게 새로고침, 페이지 종료, 외부 이동을 감지할 수 있는 방법이다.

앞에서 소개한 beforeunload 이벤트만으로는 Next.js의 내부 라우팅을 막을 수 없다. Link 컴포넌트나 router.push()를 사용할 경우, 새로고침 없이 클라이언트 측 라우팅이 일어나기 때문이다.

이 경우에는 router.push()를 오버라이드(재정의)하여 사용자 확인을 받는 방식으로 감지할 수 있다.

앞선 코드에 이어서 코드를 작성해보겠다.

const router = useRouter();

useEffect(() => {
  const warningMessage = '작성하던 내용이 모두 사라집니다. 계속하시겠습니까?';

  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
    event.preventDefault();
    event.returnValue = '';
    return warningMessage;
  };

  const originalPush = router.push;
  const newPush = (href: string): void => {
    if (!confirm(warningMessage)) {
      return;
    }
    originalPush(href);
  };

  if (typeof window !== 'undefined') {
    window.addEventListener('beforeunload', handleBeforeUnload);
    router.push = newPush;
  }

  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
    router.push = originalPush;
  };
}, [router]);

위 코드의 동작을 하나씩 살펴보면

  • originalPush: 기존의 router.push() 함수를 저장해둔다.

  • newPush: 내부 이동 시 사용자가 확인창에서 "확인"을 눌렀을 경우에만 기존 push 함수가 실행되도록 만든다.

  • router.push = newPush: 기존의 push를 우리가 정의한 함수로 덮어씌운다.

  • return 구문에서는 push를 원래대로 복구하여 메모리 누수나 예기치 못한 부작용을 방지한다.

이렇게 하면 클라이언트 사이드에서 발생하는 router.push()Link 기반 라우팅도 안전하게 차단할 수 있다.

브라우저의 뒤로가기/앞으로가기 버튼

마지막으로, 브라우저 기본 기능인 뒤로가기/앞으로가기 버튼을 통한 페이지 이동도 방지해보자.

const router = useRouter();

useEffect(() => {
  const warningMessage = '작성하던 내용이 모두 사라집니다. 계속하시겠습니까?';

  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
    event.preventDefault();
    event.returnValue = '';
    return warningMessage;
  };

  const handlePopState = (event: PopStateEvent) => {
    if (!confirm(warningMessage)) {
      history.pushState(null, '', window.location.href);
      return;
    }
    history.back();
  };

  const originalPush = router.push;
  const newPush = (href: string): void => {
    if (!confirm(warningMessage)) {
      return;
    }
    originalPush(href);
  };

  if (typeof window !== 'undefined') {
    window.addEventListener('beforeunload', handleBeforeUnload);
    window.addEventListener('popstate', handlePopState);
    history.pushState(null, '', window.location.href);
    router.push = newPush;
  }

  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
    window.removeEventListener('popstate', handlePopState);
    router.push = originalPush;
  };
}, [router]);

위 코드의 핵심을 살펴보면

  • handlePopState: 사용자가 뒤로가기를 시도할 때 confirm() 창을 띄우고, 취소할 경우 다시 현재 위치로 되돌린다.

  • history.pushState(null, '', window.location.href): 현재 주소를 히스토리에 강제로 한 번 더 푸시하여, popstate 이벤트가 트리거되도록 만든다.

이렇게 하면 사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 눌러 이동하려고 할 때도, 이탈 경고 메시지를 띄우고 확인을 받기 전까지는 이동하지 못하도록 막을 수 있다.

마무리

오늘은 이렇게 다양한 방식의 페이지 이탈 방지를 적용해보았다. 처음에는 beforeunload 이벤트만 사용하면 모든 상황을 막을 수 있을 줄 알고, 간단한 작업일 거라고 생각했지만, 막상 시작해보니 생각보다 훨씬 복잡한 작업이었다.

게다가 참고할 수 있는 자료들도 대부분 Page Router 기준으로 작성된 것이 많아서, App Router 기준으로 적용하기까지 시간이 꽤 걸렸다. 그래도 구현해보고 정리해보는 과정이 재미있었고, 오랜만에 개발하면서 제대로 뿌듯함을 느낄 수 있는 작업이었다.

가장 많이 참고한 자료
https://mxx-kor.github.io/blog/confirm-leaving-page

2개의 댓글

comment-user-thumbnail
2025년 6월 19일

레전털! 너무 흥미로워요!

1개의 답글