April 07, 2021
오늘은 Next.js 의 router
를 이용해 사용자가 페이지를 벗어나려고 하는지 감지할 수 있는 방법에 대해 기록하고자 합니다.
먼저 요구사항은 아래와 같습니다.
여러 페이지에서 쓰일 수 있으므로 커스텀훅스로 구현하도록 하겠습니다.
먼저 첫번째 요구사항부터 해결해봅니다.
뒤로가기 / 앞으로가기 / 탭 닫기 연산은 어플리케이션 내부에서 라우팅이 변경되는게 아니라 브라우저 History 자체를 변경해주는 것입니다. 따라서 이는 window의 이벤트리스너로 해결이 가능합니다.
해당 이벤트를 간단히 소개하자면 아래와 같습니다.
위의 특징에 따라 사용자가 뒤로가기 - 앞으로가기 혹은 탭 닫기 버튼을 눌렀을 때 이벤트가 트리거 되고, 우리는 alert 창을 띄워서 사용자에게 unload 이벤트를 취소할 것인지, 아니면 해당 페이지를 벗어날 것인지 물어볼 수 있습니다.
beforeunload 이벤트가 트리거 되었을 때, alert 창을 띄우기 위해서는 아래와 같이 이벤트 핸들러에서 e.preventDefault
를 실행 해주어야 합니다.
const handleWindowClose = e => {
e.preventDefault()
}
window.addEventListener('beforeunload', handleWindowClose)
alert 창 내부 메시지를 바꾸기 위해서는 아래와 같이 e.returnValue
에 원하는 메시지를 넣어주시면 됩니다.
const handleWindowClose = e => {
e.preventDefault()
return (e.returnValue = '변경사항이 저장되지 않습니다. 나가시겠습니까>')
}
window.addEventListener('beforeunload', handleWindowClose)
그렇다면 next.js 내부에서 라우팅 변경은 어떻게 감지할 수 있을까요? 위의 설정만으로는, Next.js의 Link나 router.push 등에 의한 주소 변경을 감지 할 수 없습니다.
대신 Next.js의 router에는 아래와 같은 이벤트가 있고 이에 대하여 핸들러를 등록 할 수 있습니다 .
라우팅 변경이 시작 될 때 트리거 됩니다. 이 역시 이벤트가 트리거 되었을 때 beforeunload 이벤트 처럼 중단 시킬 수 있습니다. 현재 구현상황에서는 쓰이지 않지만, routeChangeComplete 이벤트도 존재합니다. 라우팅이 완료되었을 때 트리거 됩니다.
그렇다면 Next.js에서는 어떻게 해당 이벤트를 중단시킬 수 있을까요? 아래와 같은 로직을 따릅니다.
먼저 기본값으로 leaveConfirm 을 false 로 저장해놓습니다.
아래와 같이 구현할 수 있습니다. router.events.emit
을 통해서 현재 실행된 이벤트를 취소하고, 에러를 throw 합니다.
const hanlder = () => {
if (leaveConfirm.current) return
if (window.confirm(PAGE_LEAVE_WARNING)) {
leaveConfirm.current = true
} else {
router.events.emit('routeChangeError')
throw 'routeChange aborted.'
}
}
router.events.on('routeChangeStart', hanlder)
위의 routeChangeStart 이벤트에는 문제점이 하나 있습니다.
예를 들어, write
페이지에서 위와 같이 페이지를 벗어날 때 alert을 준다고 했을 때, submit
버튼을 누른 후 다른 페이지로 router.push
를 설정해주었으면 이 때 역시도 alert 창이 뜨게 됩니다.
즉, 페이지별로 예외 사항(=예외 url)을 설정해주어야 합니다. 예외 url로 이동할 때는 alert이 뜨지 않게 말입니다.
따라서 커스텀 훅스의 props로 exception
을 받아와서, 이동하려는 페이지와 exception이 일치하는지 확인합니다. 만약 일치한다면 바로 이동해주도록 합니다.
이동하려는 페이지의 주소는 handler의 파라미터 값으로 들어옵니다.
따라서 위의 예외사항까지 더해서 커스텀 훅스로 구현하면 아래와 같이 구현됩니다.
import { useEffect, useRef } from 'react'
import { PAGE_LEAVE_WARNING } from 'common/constant/string'
import { useRouter } from 'next/router'
const useWarnUsavedChange = (exception?: string) => {
const router = useRouter()
const leaveConfirm = useRef<boolean>(false)
useEffect(() => {
leaveConfirm.current = false
}, [])
// pop up when try to leave page inner app
useEffect(() => {
const hanlder = (route: string) => {
// exception 이 존재하고, 변경하려는 route가 exception을
// 포함한다면 라우트 변경
if (exception && route.includes(exception)) return
if (leaveConfirm.current) return
if (window.confirm(PAGE_LEAVE_WARNING)) {
leaveConfirm.current = true
} else {
router.events.emit('routeChangeError')
throw 'routeChange aborted.'
}
}
router.events.on('routeChangeStart', hanlder)
return () => router.events.off('routeChangeStart', hanlder)
}, [])
useEffect(() => {
const handleWindowClose = e => {
e.preventDefault()
return (e.returnValue = PAGE_LEAVE_WARNING)
}
window.addEventListener('beforeunload', handleWindowClose)
return () => window.removeEventListener('beforeunload', handleWindowClose)
}, [])
}
export default useWarnUsavedChange