사용자의 페이지 이탈 감지하기

issol·2023년 7월 22일
0

Next

목록 보기
3/3

AS IS

폼에 새로 입력하거나 수정하는 페이지에서 사용자가 이탈 시 모달을 띄워줘야 했다.
하지만 페이지 하나하나 버튼에 액션을 넣어주기는 비효율적이라고 판단했고, 공통적으로 관리할 수 있는 hook으로 빼고 싶었다. 또한 뒤로가기 버튼(브라우저 뒤로가기 아님)만 액션을 감지하고 있었지만, 요구사항은 브라우저의 탭닫기, 브라우저 뒤로가기, 새로고침에 대한 액션도 감지하고 막을 수 있어야 했다. 이런 것들을 한번에 관리를 하고 싶었고, _app.tsx나 layout으로 빼고 싶었다. 그런데 또 한가지 고민은 수정이 안되었으면 막지 않아도 된다는 요구사항을 충족시키려면 페이지 안쪽에서 폼의 isDirty나 데이터 비교가 필요했고, 그렇다면 루트에서 뻗어 나가면 안되겠구나 해서 어디서든 간편하게 가져다 쓸 수 있는 hook으로 빼자! 로 결론이 났다.

TO BE

우선

const [hasConfirmed, setHasConfirmed] = useState(false)

  const [navigationConfig, setNavigationConfig] = useState<{
    nextRoute: string | null
    isModalOpen: boolean
  }>({
    nextRoute: null,
    isModalOpen: false,
  })

기본적으로 관리할 state를 정의해주었다.

  • hasConfirmed : 모달이 떴을 때, 유저가 나갈건지에 대한 응답
  • navigationConfig : 그럼 나갔을 때 이동할 주소가 있다면 nextRoute에 넣어주고, 모달의 상태 관리

그리고 나서 props를 정의해주었다.

export const useConfirmLeave = ({
  shouldWarn,
  toUrl,
}: {
  shouldWarn: boolean
  toUrl: string
}) => {

	.... 
}
  • shouldWarn : 페이지 내에서 폼이 수정되었고 페이지 이동감지가 되면 true, save해서 생성 후 페이지 이동이라면 false로 주어야 해서 받는 값
  • toUrl : 이동할 페이지

그리고 브라우저의 이벤트를 감지하는 함수를 넣어줬다. shouldWarn이 바뀔 때마다 실행된다.

  useEffect(() => {
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!shouldWarn) return

      e.preventDefault()
      const event = e || window.event

      return (event.returnValue =
        'Are you sure you want to leave this page? Changes you made may not be saved.')
    }

    window.addEventListener('beforeunload', handleWindowClose)

    return () => {
      window.removeEventListener('beforeunload', handleWindowClose)
    }
  }, [shouldWarn])

e.preventDefault()로 브라우저의 기본 동작(새로고침, 탭 닫기)을 막고 returnValue에 메시지를 리턴할 수 있습니다. 브라우저의 기본 alert 대신 모달을 띄우고 싶었는데, 아무리 구글링해도 되는 방법을 찾을 수는 없더라구요 ㅠㅠ


이제 새로고침, 탭닫기 시에는 해당 브라우저의 기본 alert창을 띄워 사용자의 액션을 멈춰 세울 수 있습니다.
다음으로는 뒤로가기, 실제 페이지 내의 뒤로가기나 메뉴 이동등에 대한 액션을 막아야 했습니다.

  useEffect(() => {
    const onRouteChangeStart = (route: string) => {
      if (
        decodeURI(router.asPath).split('?')[0] !==
          decodeURI(route).split('?')[0] &&
        shouldWarn &&
        !hasConfirmed
      ) {
        setNavigationConfig({
          nextRoute: route === toUrl ? toUrl : route,
          isModalOpen: true,
        })
        router.events.emit('routeChangeError', 'navigation aborted', route)
        // eslint-disable-next-line @typescript-eslint/no-throw-literal
        throw 'navigation aborted'
      }
    }

    router.events.on('routeChangeStart', onRouteChangeStart)

    const cleanUp = () => {
      router.events.off('routeChangeStart', onRouteChangeStart)
    }

    if (hasConfirmed) {
      if (!navigationConfig.nextRoute) return
      router.push(navigationConfig.nextRoute)
      return cleanUp
    }

    return cleanUp
  }, [navigationConfig, hasConfirmed, router, shouldWarn, toUrl])

next/router의 이벤트를 on 하고 파라미터로 routeChangeStart를 넣고 실행할 함수를 작성해줫습니다. 물론 cleanUp을 꼭 해주셔야 합니다.

next.js Router Event

Router 객체에는 라우팅 내부에서 발생하는 다양한 이벤트에 리스너를 등록할 수 있다. 지원되는 이벤트의 종류는 다음과 같다.

  • routeChangeStart(url, { shallow }) - 경로가 변경되기 시작할 때 발생
  • routeChangeComplete(url, { shallow }) - 경로가 완전히 변경되면 발생
  • routeChangeError(err, url, { shallow }) - 경로 변경 시 오류가 발생하거나 경로 - 로드가 취소되면 발생
  • err.cancelled - 탐색이 취소되었는지 여부를 나타냄
  • beforeHistoryChange(url, { shallow }) - 브라우저의 기록을 변경하기 전에 실행
  • hashChangeStart(url, { shallow }) - 해시는 변경되지만 페이지는 변경되지 않을 때 발생
  • hashChangeComplete(url, { shallow }) - 해시가 변경되었지만 페이지가 변경되지 않은 경우 발생

router의 asPath뒤에 queryString이 붙은 경우까지 생각해서 경로가 이동하려는 경로 (route)와 진짜 달라진건지에 대한 비교를 하고, 만약 shouldWarn이 true이고 아직 확인 버튼을 누르기 전이라면 모달을 띄우도록 했다. 이 경우엔 사용자를 멈춰 세우기 위해 throw 'navigation aborted' 이렇게 강제적으로 에러를 내야 한다. 만약 hasConfirmed가 ture가 되고 갈 route가 있다면 이동시키고 아니라면 유지한다. 그리고 이벤트를 on 시켜줬으니 클린업을 해서 Off를 꼭 시켜줘야 한다.

const ConfirmLeaveModal = () =>{
   <Dialog open={navigationConfig.isModalOpen} maxWidth='lg'>
     	.....
   </Dialog>
}
  return {
    ConfirmLeaveModal,
  }

그리고 마지막에 띄워줄 모달을 리턴해주면 된다.
실제 이 모달이 필요한 페이지의 부모에서 hook을 불러서 모달을 리턴해주면 사용자의 페이지 이탈 액션을 감지해서 한번 막아줄 수 있다.

  const { ConfirmLeaveModal } = useConfirmLeave({
    // shouldWarn안에 isDirty나 isSubmitting으로 조건 줄 수 있음
    shouldWarn: true,
    toUrl: '/',
  })
  
  return  <ConfirmLeaveModal />
profile
프론트 엔드 개발자

0개의 댓글