WYSIWYG 에디터를 간단하게 만들어봅니다
[소소한 개발 일지] popstate 이벤트에서 앞으로가기, 뒤로가기 구분하기
2023-10-21
Explanation
오늘은 popstate 이벤트에서 앞으로가기, 뒤로가기를 구분하는 방법에 대해 적어보려 합니다.
바닐라로 적을지 리액트로 적을지 고민하다가 바닐라가 기본이긴 하지만 보편적으로 리액트를 더 많이 쓸 꺼 같아서 리액트로 적었습니다!
작성된 코드는 https://github.com/falsy/blog-post-example/blob/master/popstate-direction/src/index.js 에서 확인하실 수 있습니다!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
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 /> ) |
1 2 3 4 |
const App = () => { const [historyIndex, setHistoryIndex] = useState(0) const [direction, setDirection] = useState('None') ... |
histroyIndex는 히스토리가 증감할 때마다 index 값을 늘리고 줄여줘서 이 값을 기준으로 popstate에서 앞, 뒤 구분을 해줄 거에요.
1 2 3 4 5 6 7 8 9 |
useEffect(() => { const index = window.history.state?.index if(index) { setHistoryIndex(index) } else { window.history.replaceState({ index: 0 }, '') } }, []) ... |
처음 페이지에 들어오면 history 객체의 state 값에 index 값이 있는지 확인합니다.
(새로고침으로 접근한 거라면 기존에 입력한 index 값이 있을 거예요.)
그리고 없다면 index 값을 초깃값으로 0으로 replaceState 해줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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를 재정의해 줄 거에요. 왜냐면…
글을 적다 보니… 문득,
생각해 보니 그냥 서비스 내에서 사용하는 거라면 굳이 재정의 하지 말고 별개로 함수를 만들어 쓰는 게 더 좋을 거 같네요.
1 2 3 4 5 6 |
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 값을 디펜던시로 추가해 주었습니다.
1 2 3 4 5 6 7 8 9 |
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() 해주어서 다음 기준값으로 등록해 줍니다.