-
Notifications
You must be signed in to change notification settings - Fork 1
뒤로가기 confirm 로직 구현하기
react-router-dom v5에서는 history 라이브러리를 사용해서 직접 history 객체에 접근할 수 있었다. 그래서 history 객체의 block API를 사용해서 뒤로가기를 막을 수 있었다.
import React, { useEffect } from 'react';
import { Router, Route, Routes, useLocation } from 'react-router-dom';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
function App() {
return (
<Router history={history}>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
}
function Home() {
useBlockNavigation();
return <h2>Home</h2>;
}
function useBlockNavigation() {
const location = useLocation();
useEffect(() => {
const unblock = history.block((tx) => {
if (window.confirm("정말 이 페이지를 떠나시겠습니까?")) {
unblock();
tx.retry();
} else {
return false;
}
});
return () => unblock();
}, [location]);
}
export default App;
그런데 react-router-dom v6부터는 useNavigate hook을 사용해서 navigate 함수를 기반으로 경로를 이동하기 때문에 history 객체를 직접적으로 사용할 수 없다.
다른 방법을 찾아보던 중 브라우저의 popstate
이벤트를 활용해보기로 했다. popstate
이벤트는 브라우저의 history stack이 변할때 발생하는 이벤트인데, 브라우저의 뒤로 가기나 앞으로 가기 버튼을 클릭할 때 발생한다. 뒤로가기 버튼이 트리거될 때 해당 이벤트가 발생하니까 이벤트가 발생할 때 뒤로가기를 prevent 해서 confirm 창을 띄우고 사용자가 뒤로가기에 동의하면 pop, 뒤로가기에 동의하지 않으면 그대로 경로에 남아있도록 구현하면 되겠다고 생각했다.
popstate 이벤트는 preventDefault()
를 사용할 수 없다. popstate
이벤트는 단순히 브라우저의 히스토리 상태가 변경될 때 발생하며, 이 이벤트는 브라우저의 기본 동작을 직접적으로 막을 수 없기 때문이다.
따라서 preventDefault를 직접 하는 것이 아니라 그렇게 보이도록 우회해서 만들어야한다.
import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
export default function useConfirmNavigation(message: string) {
const navigate = useNavigate();
const location = useLocation();
const [preventBack, setPreventBack] = useState(false);
useEffect(() => {
const handlePopState = (e: PopStateEvent) => {
if (preventBack) {
const confirm = window.confirm(message);
if (!confirm) {
navigate(location.pathname, { replace: true });
}
}
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, [navigate, location.pathname, message, preventBack]);
useEffect(() => {
setPreventBack(true);
return () => {
setPreventBack(false);
};
}, []);
}
이런 식으로 구현할 수 있는데, 하나하나 살펴보자면 사용자가 브라우저의 뒤로가기 버튼을 클릭하면 popstate
이벤트가 발생한다. handlePopState 내에서는 window.confirm(message)
를 사용하여 사용자에게 확인 대화 상자를 표시한다. 사용자가 취소
를 선택하면 !confirm
이 true
가 되고, navigate(location.pathname, { replace: true })
를 통해 현재 페이지로 다시 이동한다.
_._2024-08-08_23.18.53.mov
뒤로 가기를 여러번 누르는 경우 뒤로 가기 방지 confirm 창이 뜨지 않고, 또 앞으로 가기를 하는 경우에도 뒤로가기 방지 창이 뜨는 등 이상하게 동작했다.
그 이유를 생각해보았는데, popstate
이벤트는 브라우저에서 뒤로 가기 버튼을 누를 때 뿐만 아니라 앞으로 가기 버튼을 클릭할 때 등 여러 상황에서 발생한다. 결국 popstate
이벤트는 브라우저의 히스토리 스택의 상태가 변경될 때 발생하고 브라우저의 이동 동작을 제어하는 기능이 아니기 때문에 우회해서 구현하는 방식으로는 한계가 있는 것으로 판단했다.
react-router-dom에서 제공하는 usePrompt hook은 페이지 이탈 시 사용자에게 경고 메시지를 표시하는 데 사용된다. 페이지 이탈 전 발생하는 beforeunload
이벤트와 어떤 차이가 있나 싶었는데, usePrompt는 react router의 네비게이션 context에서 동작하기 때문에 spa의 history 제어를 더 적합하게 처리할 수 있다고 볼 수 있다.
import { useEffect, useState } from "react";
import { unstable_usePrompt, useLocation } from "react-router-dom";
export function useBlockNavigation(message: string) {
const location = useLocation();
const [isBlocking, setIsBlocking] = useState(false);
unstable_usePrompt({ when: isBlocking, message });
const unblockNavigation = () => {
setIsBlocking(false);
};
useEffect(() => {
setIsBlocking(true);
return () => {
setIsBlocking(false);
};
}, [location]);
return { unblockNavigation };
}
usePrompt hook을 기반으로 위와 같이 뒤로가기 방지 코드를 작성할 수 있다.
unstable_usePrompt({ when: isBlocking, message });
이 코드가 핵심 코드이다. when은 prompt의 활성화 여부를 의미하고 message는 prompt에 나타날 메시지이다. 해당 hook을 선언해두면 hook이 선언된 페이지에서 벗어나는 경우 prompt가 뜨게 되고 뒤로가기 여부를 사용자에게 확인받을 수 있게 된다.
추가)
useEffect(() => {
setIsBlocking(true);
return () => {
setIsBlocking(false);
};
}, [location]);
또한 이 코드는 마운트/언마운트 시 뒤로가기 동작 blocking을 제어하기 위한 코드이다. 컴포넌트가 마운트되면 preventBack
이 true
로 설정되어 뒤로가기 방지를 하고, 컴포넌트가 언마운트되면 preventBack
이 false
로 설정되어 뒤로가기 버튼이 정상적으로 동작한다.
❗️ 이후 수정사항
alert가 UX적으로 좋지 않은 것 같다는 피드백을 받아 직접 useBlocker를 사용해서 alert 창을 커스텀 하도록 수정했다.
export function useBlockNavigation() {
const location = useLocation();
const [isBlocking, setIsBlocking] = useState(false);
const blocker = useBlocker(isBlocking);
const isBlockedNavigation = blocker.state === "blocked";
const unblockNavigation = () => {
setIsBlocking(false);
};
const cancelNavigation = () => {
if (blocker.state === "blocked") {
blocker.reset();
}
};
const proceedNavigation = () => {
if (blocker.state === "blocked") {
blocker.proceed();
}
};
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isBlocking) {
e.preventDefault();
e.returnValue = "";
}
};
useEffect(() => {
setIsBlocking(true);
return () => {
setIsBlocking(false);
};
}, [location]);
useEffect(() => {
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [isBlocking]);
return { unblockNavigation, isBlockedNavigation, proceedNavigation, cancelNavigation };
}
- 페이지가 렌더링 된 후 스크롤을 최상단으로 올리기
- DOM을 이미지로 저장하기
- Context API에서 불필요한 리렌더링을 줄이는 방법
- 무한 Transition 애니메이션
- Github Action 워크플로우 정의하기
- 무한 Transition 애니메이션 최적화하기
- 무한 Transition 애니메이션 최적화하기 2
- 무한 Transition 애니메이션 최적화하기 3
- fetch timeout 구현하기
- ErrorBoundary 구현하기
- 뒤로가기 confirm 로직 구현하기
- 선착순 밸런스 게임 상태 관리
- Modal 내부 컴포넌트에서 무한 스크롤이 제대로 동작 안되는 문제
- useToggleContents 훅 기본값 설정 및 조건부 사용법
- 폰트 굵기 적용 이슈
- SVG 내부 stroke 속성 값 제어를 위한 SVGIcon Util 함수 및 SVGR 사용 과정
- tailwindCSS의 @apply를 cva로 바꾸기
- 스크롤 내려갈 때 해당 섹션의 요소들 인터렉션 동작
- 공통 컴포넌트 내부에 애니메이션을 넣는 것에 대한 고민
- 특정 컴포넌트 위치에서 헤더 스타일 다르게 적용하는 방법
- 스크롤 거꾸로 올릴 때 IntersectionObserver가 뷰포트 감지 못하는 현상
- 선착순 밸런스 게임 최종 결과 계산에 대한 고민 (08.14)
- 프로그래스바 공통 컴포넌트로 분리
- (08.23 기준) 선착순 서버 시간 연동 문제
- 게임 종료된 상태에서 사용자 게임 참여 여부에 따른 FinalResult 분기 처리
- FinalResult 컴포넌트의 “당신의 선택” 카테고리 설정 이슈
- 게임 접속 시 게임 현재 진행 상태 초기화 및 카운트 다운 설정
- 선착순 밸런스 게임 UX 개선