Next router 이동 막기 _알림창

2022. 12. 4. 18:18Web_Programming/Next

많은 사이트에서 글을 작성하고나 편집할 때, 페이지 이동시 알림 창을 띄우는 UI를 많이 봤습니다.

해당 기능을 개발하면서 생각보다 골치 아팠고 처음 알게 된 처리방법을 찾아 포스팅을 해보려 합니다-

 

구현 요구사항에 따른 처리 프로세스는 크게 세 가지가 있어요.

  • Link 컴포넌트를 통한 이동 막기
  • 새로고침 이동 막기
  • 뒤로 가기 막기

하나 씩 살펴보겠습니다-


Link를 통한 이동 막기

 

next/router 자체에서 라우팅 이벤트를 제공하고 있어요.

 

next/router | Next.js

Learn more about the API of the Next.js Router, and access the router instance in your page with the useRouter hook.

nextjs.org

  • routeChangeStart(url, { shallow }) - Fires when a route starts to change
  • routeChangeComplete(url, { shallow }) - Fires when a route changed completely
  • routeChangeError(err, url, { shallow }) - Fires when there's an error when changing routes, or a route load is cancelled
    • err.cancelled - Indicates if the navigation was cancelled
  •  beforeHistoryChange (url, { shallow }) - Fires before changing the browser's history
  • hashChangeStart(url, { shallow }) - Fires when the hash will change but not the page
  • hashChangeComplete(url, { shallow }) - Fires when the hash has changed but not the page

 

여기서 저는  beforeHistoryChange 이벤트 리스너를 사용하여

 

Link 컴포넌트에 의한 history 변경이 감지되면,

  1. 이동 확인을 위한 모달 Alert 창 open
  2. routerChange 중지
  3. 모달 Alert 창에서 처리
    1. 모달 창에서 이동 취소 ➞ 모달 close
    2. 모달 창에서 이동 확인 ➞ 이동 확인 버튼 Link컴포넌트에 변경 url 주입

저는 routePath라는 state에 change 될 url을 update 하여 이후 모달의 이동 버튼 Link컴포넌트에 주입시켜 이동을 시켰습니다.

 

const historyChangeHandler = (url: string) => {
    if (url === router.asPath) return;
    
    if (!isModalOpen) {
      setRoutePath(url);
      openModal();
      
      //페이지 이동 block
      router.events.emit("routeChangeError");
      throw "routeChange aborted.";
    }
  };

  useEffect(() => {
    router.events.on("beforeHistoryChange", historyChangeHandler);

    return () => {
      router.events.off("beforeHistoryChange", historyChangeHandler);
    };
  },[]);

 


새로고침 막기

새로고침 처리는 많은 곳에서 사용되기에 정보도 많아 쉽게 처리할 수 있었어요.

 

window 객체에서 가지고 있는 onbeforeunload handler에 true를 반환시켜주면 

브라우저에서 제공하는 새로고침 confirm 모달을 띄울 수 있습니다.

  useEffect(() => {
    router.events.on("beforeHistoryChange", historyChangeHandler);
    window.onbeforeunload = () => true;

    return () => {
      router.events.off("beforeHistoryChange", historyChangeHandler);
      window.onbeforeunload = null;
    };
  }, []);


뒤로 가기 막기

 

가장 고민을 많이 했던 것이 뒤로 가기 막기인데요.

단순히 뒤로 가기를 막는 것이 아니고 아래 두 가지를 지켜해서 어려웠어요.

 

  • 모달 Alert 창에서 이동을 클릭하면 뒤로 가기는 그대로 수행
  • history가 깨지면 안 됨

 

심지어 뒤로 가기는 beforeHistoryChange에 잡히기 전에 url이 변경되어 버리는 문제가 있었고,

 

이는 next/router에서 제공하는 beforePopState로 뒤로 가기 전에 콜백 함수로 시행시켜 해결하고자 했습니다.

 

next/router | Next.js

Learn more about the API of the Next.js Router, and access the router instance in your page with the useRouter hook.

nextjs.org

 

 

beforePopState에서 임의로 현재 페이지를 push 해주면 된다 생각했고,

refresh 없이 수행되어야 했기에 이를 가능케 하는 방법으로 window.history 에서 제공하는 pushState라는 함수가 있습니다.

 

 

History.pushState() - Web APIs | MDN

In an HTML document, the history.pushState() method adds an entry to the browser's session history stack.

developer.mozilla.org

   const historyChangeHandler = (url: string) => {
    if (!isModalOpen) {
      setRoutePath(url);
      openModal();
      
      //페이지 이동 block
      router.events.emit("routeChangeError");
      throw "routeChange aborted.";
    }
    if (isPopState) {
      //모달에서 뒤로가기
      router.back();
    }
  };
  /**
   * @decription 페이지 새로고침 전 url을 잡아두기 위한 handler
   * @param {string} url 뒤로가기 후 이동될 url
   */
  const blockPopStatehandler = ({ url }: { url: string }) => {
    if (router.asPath !== url) {
      window.history.pushState(null, "", router.asPath);
      setIsPopState(true);
    }
    return true;
  };

  useEffect(() => {
    router.events.on("beforeHistoryChange", historyChangeHandler);

    router.beforePopState(blockPopStatehandler);
    window.onbeforeunload = () => true;

    return () => {
      router.events.off("beforeHistoryChange", historyChangeHandler);
      router.beforePopState(() => true);
      window.onbeforeunload = null;
    };

  }, []);

 

뒤로 가기 beforePopState 이벤트 감지 후

  1. 현재 url path push하여 뒤로 가기 상쇄
  2. 모달 Alert창 open
    1. 모달 창에서 이동 취소 ➞ 모달 close
    2. 모달 창에서 이동 확인 ➞ router.back 수행

 

하지만 해당 로직에도 문제가 하나 있어요.

 

뒤로 가고(back) 앞으로 돌아올 때(forward) url만 변경되고 rerender가 되지 않아요.

 

원인을 생각해 봤을 때,

애초에 pushState로 history에 넣은 path가 refresh 없이 수행하도록 들어가 있어서

앞으로 가기에서도 해당 history가 refresh를 수행하지 않는 것 같아요.

 

하지만 이외에 rendering 없이 url을 변경하는 방법은 여러 시도를 해봐도 찾지 못했어요.

 

그래서 url 변경은 보여주고 동작을 제대로 수행되도록 수정했는데요.

 

뒤로 가기가 감지 되면

  1. 뒤로 가기 URL 유지
  2. 모달 Alert창 open
    1. 모달 창에서 이동 취소  window.history.forward 앞으로 가기 수행
    2. 모달 창에서 이동 확인 ➞ rerender로 뒤로가기
 useEffect(() => {
    if (!isModalOpen && router.asPath !== window.location.pathname) {
        window.history.forward();
    }
  }, [isModalOpen]);

 

 

모든 사항을 만족시키지는 못했지만 동작의 정상적인 구현이 우선으로 생각하여 위와 같이 마무리하였습니다.

 

작업해 보면서 history를 다루는 방법과

처음 알게 된 pushSate 및 router에서 제공하는 event 리스너 등을 사용해 보고 공부해 볼 수 있었습니다.

반응형