Next.js에서 뒤로가기 방지 구현하기

starp·2024년 1월 17일
1

브라우저의 뒤로가기 동작 방식

뒤로가기를 막기 위해서는 일단 브라우저의 뒤로가기 동작 방식에 대해 알 필요가 있다. 전체적인 흐름은 다음과 같다.

  1. 사용자가 뒤로가기 클릭 (단축키, 터치패드, 버튼클릭 등등...)
  2. 브라우저의 history 변경 (가장 최신 url - 즉, 현재 url pop)
  3. 브라우저의 popState 이벤트 발생
  4. 화면 뒤로가기

뒤로가기 감지하기

Next.js의 page router에선 router object가 제공하는 beforePopState 함수를 통해 브라우저의 뒤로가기를 감지할 수 있다.
안타깝게도 app router에선 next/router 대신 next/navigation의 useRouter()를 사용한다고 하니 다른 방식을 찾아봐야 할 것 같다. 아래 링크에서 app router로 뒤로가기 방지를 구현하신 분이 있으니 참고해보자.
관련 링크 : https://velog.io/@naro-kim/Nextjs13-App-dir에서-페이지-이탈-방지-모달을-만들고-route.events-대체하기

하지만 현재 진행하는 프로젝트는 page router를 사용하기 때문에 beforePopState를 통해 구현하겠다.

beforePopState

beforePopState 함수는 말 그대로 popState 이벤트 이전에 동작하는 함수이다.
만약 beforePopState 함수의 콜백이 false를 return하면 뒤로가기를 하지 않고, true를 return하면 뒤로가기가 실행된다.

공식 문서 : https://nextjs.org/docs/pages/api-reference/functions/use-router#routerbeforepopstate

이는 다시 말하면, popState 이전에 일어나는 일에 대해서는 이 함수로 막을 수 없다는 의미이다.
그렇다면 popState 이전에 일어나는 일이란 무엇인가 하니, 위에 브라우저 뒤로가기 동작방식 에서 설명했던 것처럼 브라우저의 history가 변경되는 일이 바로 그것이다.
즉, beforePopState의 콜백의 리턴값을 false로 해서 뒤로가기를 막는다고 하여도, 현재 사용자가 위치한 페이지의 url은 pop되어 사라지고, 뒤로가기가 성공했을 때의 url이 주소창에 남아서 history가 꼬여버릴 수 있다.

history push하기

그렇다면 해야할 일은 pop된 현재 url을 history에 다시 push해주면 된다.
이는 window.history.pushState()를 통해 할 수 있다.
지금까지의 흐름을 토대로 코드를 작성하면 아래와 같다.

const router = useRouter()

  useEffect(() => {
    router.beforePopState(() => {
      window.history.pushState(null, '', router.asPath)
      // 현재 url(router.asPath)을 history에 push

      return confirm('뒤로가기 시 페이지 내용은 저장되지 않습니다. 이동하시겠습니까?')
      // 사용자가 예를 누르면 true, 아니오를 누르면 false 리턴
    })

    return () => {
      router.beforePopState(() => true)
    }
    // beforePopState을 true로 하여 브라우저가 원래대로 동작하도록 cleanup
  }, [router])

일단 위 코드와 같이 브라우저의 window 객체에서 제공하는 confirm메서드를 활용하는 것으로 쉽게 구현할 수 있다.
그런데 만약 제공되는 confirm 창 말고, 내가 만든 모달을 통해 구현하고 싶다면 어떻게 해야할까?

내가 만든 모달을 활용해 구현하기

처음 생각한 흐름은 다음과 같다.
1. 모달을 여닫는 상태, 뒤로가기를 감지할 상태를 따로 선언
2. 사용자가 뒤로가기 클릭 시 모달이 열림
3. 모달에서 예를 클릭하면 모달이 닫히면서 뒤로가기 상태가 true로 변경
4. useEffect에서 상태 변화를 감지하여 true 리턴
5. 아니오를 클릭하면 false리턴

const router = useRouter()
const [openModal, setOpenModal] = useState(false)
const [prevPage, setPrevPage] = useState(false)

  useEffect(() => {
    router.beforePopState(() => {
      window.history.pushState(null, '', router.asPath)
      setOpenModal(true)
      
	  if (prevPage) {
        return true
      }
      
      return false 
    })

    return () => {
      router.beforePopState(() => true)
    }
  }, [router, prevPage])

// 모달은 openModal, setOpenModal, setPrevPage를 props로 전달
// 모달 내부 버튼의 onClick 함수에서 props로 받은 set함수를 통해 state 변경

하지만 위 코드는 내가 원하는 방식대로 동작하지 않았다.
처음 뒤로가기를 하고 나타나는 모달에서 예를 눌러 prevPage가 true로 바뀌었지만 모달만 닫힐 뿐, 뒤로가기가 동작하지 않았고 다음 뒤로가기를 했을 때, 모달이 뜨는 순간 바로 뒤로가기가 실행되었다.

원인은 결국 흐름의 문제였다.
prevPage가 true가 되는 시점에서는 이미 beforePopState 내부로직은 한바퀴가 끝난 상황 -> 즉, false가 리턴된 상황이고, 이후 뒤로가기를 한번 더 눌렀을 때 비로소 prevPage가 true인 if 분기 내부를 타기 때문에 원하는대로 동작하지 않는 것이였다.

해결 및 구현

위 문제를 해결하기 위해 생각한 결론은 다음과 같다.
1. useEffect 내부에서는 현재 history를 쌓는 작업과 모달을 여는 작업, 뒤로가기를 막는 작업만 수행.
2. 모달에서 예를 클릭했을 때의 이벤트 핸들러에 뒤로가기 로직을 수행하도록 하기.

  useEffect(() => {
    router.beforePopState(() => {
      window.history.pushState(null, '', router.asPath)
      setModalOpen(true)

      return false
    })
  }, [router, setModalOpen])


// 모달 로직
export default function Modal({ open, setOpen }: Ownprops) {
  const router = useRouter()

  const closeButtonHandler = () => {
    setOpen(false)
  }
  const confirmButtonHandler = () => {
    router.beforePopState(() => true)
    // 모달이 열린 현재 beforePopState 내부의 콜백이 false를 리턴하고있는 상황이므로 
    // 반드시 true로 바꿔주어야 뒤로가기가 동작함
    router.back()
    // next router에서 제공하는 뒤로가기 메서드 사용
  }

  return (
    ...
      <div>페이지 이동 시 내용이 저장되지 않습니다. 이동하시겠습니까?</div>
        <button onClick={closeButtonHandler}>아니오</button>
        <button onClick={confirmButtonHandler}></button>
		...
  )
}

24.02.02 추가 사항


 useEffect(() => {
   router.beforePopState(() => {
      if (as !== router.asPath) {
      	window.history.pushState(null, '', router.asPath)
      	setModalOpen(true)
      }

      return false
  })
  return () => {
      router.beforePopState(() => true)
  }
  }, [router, setModalOpen])

as !== router.asPath 조건을 추가하여 조금 더 안정적으로 내부 로직이 동작하도록 변경.

useEffect의 클린업에 router.beforePopState(() => true) 을 추가함. 사용자가 뒤로가기가 아닌 다른 방식으로 이동하여 컴포넌트가 언마운트 되면 뒤로가기가 가능해지도록 하기 위함.

profile
포기하지 않고 꾸준히.

0개의 댓글

관련 채용 정보