[소소한 개발 일지] popstate 이벤트에서 앞으로가기, 뒤로가기 구분하기

Explanation

오늘은 popstate 이벤트에서 앞으로가기, 뒤로가기를 구분하는 방법에 대해 적어보려 합니다.
바닐라로 적을지 리액트로 적을지 고민하다가 바닐라가 기본이긴 하지만 보편적으로 리액트를 더 많이 쓸 꺼 같아서 리액트로 적었습니다!

작성된 코드는 https://github.com/falsy/blog-post-example/blob/master/popstate-direction/src/index.js 에서 확인하실 수 있습니다!

1. 전체 코드

import { useState, useEffect, useCallback } from 'react'
import ReactDOM from 'react-dom/client'

const container = document.getElementById('wrap')
const root = ReactDOM.createRoot(container)

const App = () => {
  const [historyIndex, setHistoryIndex] = useState(0)
  const [direction, setDirection] = useState('None')

  useEffect(() => {
    const index = window.history.state?.index
    if(index) {
      setHistoryIndex(index)
    } else {
      window.history.replaceState({ index: 0 }, '')
    }
  }, [])

  useEffect(() => {
    window.history.pushState = (state, ...arg) => {
      const index = historyIndex + 1
      setHistoryIndex(index)
      return History.prototype.pushState.apply(
        window.history, 
        [{ ...state, index }, ...arg]
      )
    }
    window.history.replaceState = (state, ...arg) => {
      return History.prototype.replaceState.apply(
        window.history, 
        [{ ...state, index: historyIndex }, ...arg]
      )
    }
    window.addEventListener('popstate', onPopstate)
    return () => {
      window.removeEventListener('popstate', onPopstate)
    }
  }, [historyIndex])

  const onPopstate = useCallback(() => {
    const { state } = window.history
    if (!state) window.history.replaceState({ index: historyIndex + 1 }, '')

    const index = state ? state.index : historyIndex + 1
    setHistoryIndex(index)
    setDirection(index > historyIndex ? 'forward' : 'back')
  }, [historyIndex])

  return (
    <div>
      <h1>Direction: {direction}</h1>
      <ul>
        <li onClick={() => window.history.pushState({}, '', '#1')}>#1</li>
        <li onClick={() => window.history.pushState({}, '', '#2')}>#2</li>
        <li onClick={() => window.history.pushState({}, '', '#3')}>#3</li>
      </ul>
    </div>
  )
}

root.render(
  <App />
)

2. 바로 시작

const App = () => {
  const [historyIndex, setHistoryIndex] = useState(0)
  const [direction, setDirection] = useState('None')
  ...

histroyIndex는 히스토리가 증감할 때마다 index 값을 늘리고 줄여줘서 이 값을 기준으로 popstate에서 앞, 뒤 구분을 해줄 거에요.

  useEffect(() => {
    const index = window.history.state?.index
    if(index) {
      setHistoryIndex(index)
    } else {
      window.history.replaceState({ index: 0 }, '')
    }
  }, [])
  ...

처음 페이지에 들어오면 history 객체의 state 값에 index 값이 있는지 확인합니다.
(새로고침으로 접근한 거라면 기존에 입력한 index 값이 있을 거예요.)
그리고 없다면 index 값을 초깃값으로 0으로 replaceState 해줍니다.

  useEffect(() => {
    window.history.pushState = (state, ...arg) => {
      const index = historyIndex + 1
      setHistoryIndex(index)
      return History.prototype.pushState.apply(
        window.history, 
        [{ ...state, index }, ...arg]
      )
    }
    window.history.replaceState = (state, ...arg) => {
      return History.prototype.replaceState.apply(
        window.history, 
        [{ ...state, index: historyIndex }, ...arg]
      )
    }
    window.addEventListener('popstate', onPopstate)
    return () => {
      window.removeEventListener('popstate', onPopstate)
    }
  }, [historyIndex])
  ...

이제 pushState와 replaceState를 재정의해 줄 거에요. 왜냐면...

글을 적다 보니... 문득,
생각해 보니 그냥 서비스 내에서 사용하는 거라면 굳이 재정의 하지 말고 별개로 함수를 만들어 쓰는 게 더 좋을 거 같네요.

  const handleClickPushState = useCallback((to) => {
    window.history.pushState({ index: historyIndex + 1}, '', to)
  }, [historyIndex])
  const handleClickReplaceState = useCallback((to) => {
    window.history.replaceSteate({ index: historyIndex }, '', to)
  }, [historyIndex])

그냥 위와 같이 함수를 만들어서 쓰는 게 더 좋겠네요.
기본 내장 객체의 함수를 재정의하는 건.. 뭔가.. 뭔가.. 하니까..

보통 이렇게 잘못된 게 있으면 글을 처음으로 돌아가 고쳐 적는 게 일반적이지만, 이 또한 또 소소한 개발 블로그 포스팅의 맛이 아니겠습니까? (라고 쓰고 귀찮아서 인건 안비밀..)

그리고 아래 popstate 이벤트를 등록해 주고 historyIndex 값을 디펜던시로 추가해 주었습니다.

  const onPopstate = useCallback(() => {
    const { state } = window.history
    if (!state) window.history.replaceState({ index: historyIndex + 1 }, '')

    const index = state ? state.index : historyIndex + 1
    setHistoryIndex(index)
    setDirection(index > historyIndex ? 'forward' : 'back')
  }, [historyIndex])
  ...

드디어 대망의? popstate 이벤트 함수를 정의해 주었습니다.
우선 state 값이 있는지 확인합니다! 그리고 state가 없다면 앞으로의 히스토리 index 비교 관리를 위해서, index 값을 historyIndex + 1로 추가서 replaceState 해줍니다.

히스토리가 추가됐는데 state 값이 없는 상황은, 예를 들면 window.location.hash = 'hello' 이렇게 해시를 추가해서 히스토리가 추가되는 경우가 있겠네요!

그리고 현재의 index 값을 state.index 값이 있다면 state.index 값으로, 없다면 historyindex + 1 으로 주면!

여기에서 index 값이 historyIndex 값보다 크면 브라우저에서 앞으로가기 했을 때가 되고 아니라면 뒤로 가기 상황이라는 걸 확인할 수 있답니다.

마지막으로 index 값을 다시 setHistoryIndex() 해주어서 다음 기준값으로 등록해 줍니다.

끝

참고. https://github.com/johanholmerin/popstate-direction