이번 포스팅에서는 웹을 개발할때 기본적으로 고려해야할 부분중에 하나인 더티 체크에 대해서 이야기 해보려고 한다.
여기서 말하는 더티 체크란 블로그 글과 같이 작성 및 수정이 가능한 페이지에서 글을 작성하는 중에 사용자의 실수로 페이지를 이탈하려는 경우 막아주는 최소한의 안전장치이다.
다들 아래와 같은 알럿창을 많이 접했을 것이다.

간단하다면 간단해 보이는 기능이지만 이 기능을 활용하는데 여러가지 고려할 사항들이 있었다. 지금부터 이 기능을 개발하면서 겪은 일들과 해결한 방법에 대해 이야기 해보겠다.
이전부터 사용하던 hook이 있었다. 코드는 다음과 같다.
import { useCallback, useEffect, useState } from 'react';
import { To, unstable_useBlocker, useNavigate } from 'react-router-dom';
interface UseBlockerProps {
isBlock?: boolean;
browserBackClick: (url: any, action: string) => boolean | void;
}
/**
*
* @param isBlock 블락 여부 판단 변수
* @param browserBackClick 뒤로가기 핸들링 함수
* @returns change navigation nextpath
*/
function useBlocker({ isBlock, browserBackClick }: UseBlockerProps) {
const navigate = useNavigate();
const [navUrl, setNavUrl] = useState<To>();
const [params, setParams] = useState<{ [key: string]: any }>();
const onSetUrl = useCallback((url: To, state?: { [key: string]: any }) => {
setNavUrl(url);
setParams(state);
}, []);
useEffect(() => {
if (navUrl === '-1') {
setTimeout(() => navigate(-1), 0);
} else if (navUrl) {
setTimeout(() => navigate(navUrl, { state: params }), 0);
}
}, [navUrl, navigate, params]);
const blockNavigation = useCallback(
(tx: any) => {
console.log(tx.historyAction, '::: history');
if (tx.historyAction === 'POP' && isBlock) {
browserBackClick(tx.nextLocation.pathname, tx.historyAction);
return true;
}
if (tx.historyAction === 'POP' && !isBlock) {
return false;
}
if (tx.historyAction === 'PUSH' && !navUrl && isBlock) {
browserBackClick(tx.nextLocation.pathname, tx.historyAction);
return true;
}
if (tx.historyAction === 'PUSH' && !navUrl && !isBlock) {
return false;
}
if (!isBlock) return false;
return false;
},
[browserBackClick, isBlock, navUrl],
);
unstable_useBlocker(blockNavigation);
const preventClose = useCallback(
(e: BeforeUnloadEvent) => {
if (isBlock) {
e.preventDefault();
e.returnValue = ''; // chrome에서는 설정이 필요해서 넣은 코드
}
},
[isBlock],
);
useEffect(() => {
window.addEventListener('beforeunload', preventClose);
return () => {
window.removeEventListener('beforeunload', preventClose);
};
}, [preventClose]);
return { onSetUrl };
}
export default useBlocker;
이때는 구글링을 하다가 잘 동작하는 코드를 발견해서 아무생각 없이 그냥 사용하고 있었다.
근데 문제는 unstable_useBlocker 라는 기능을 사용하고 있다는 것이었다. 그 당시에는 이름에 대해 크게 고민하지 않았었다.
다음 과업으로 새로 시작하는 프로젝트를 담당하게 되어서 초기 세팅을 하며 해당 hook도 그대로 가져와 사용하려고 했었다.
그런데... 해당 함수 부분을 불러오지 못한다는 에러와 마주하게 되었다. 그때서야 함수명이 눈에 들어왔다. unstalbe... 불안정한 버전의 기능이라는 표기였다....
이 하나를 위해 버전 다운그레이드를 하는것을 원치 않았기 때문에 다시 구글링을 하기 시작했다.
다행히도 정식 기능으로 등록되어 함수명 앞에 unstable이 사라진 것이었다.

같은 실수를 반복하지 않기위해 공식문서 부터 한번 쭉 살펴보았다.
useBlocker 훅에는 여러가지 유용한 기능들이 많았다.
이 훅은 첫번째 파라미터로 boolean 또는 BlockerFunction 타입을 받는다. 여기서 BlockerFunction은 ({currentLocation, nextLocation, historyAction}) => boolean 이런 형태의 함수이다.
하나씩 살펴보면 다음과 같다.
로그를 통해 알아보면 다음과 같다.

해당 예시는 /blocker-test 페이지에서 /scheduler-test 페이지로 이동하려고 시도했을때 발생하는 로직이다.
이로써 파리미터로 boolean 값을 바로 넣게되면 해당 값에 따라서 라우팅을 막을지 말지를 결정하게 되고, BlockerFunction을 넣으면 라우팅을 시도할때마다 발동하여 내부 특정 조건에 따라 라우팅을 할지 말지 결정할 수 있다는 것을 알았다.
그렇다면 해당 훅이 반환하는 값들은 어떤것들이 있을까?
unblocked 상태로 돌려놓는 함수blocked인 상태를 무시하고 라우팅을 진행하게 하는 함수nextLocation 과 같이 이동을 시도한 라우트 정보위와 같이 아주 다양한 기능들을 제공해주고 있었다. 이 값들을 활용하는 방법에 대해 알아보자.
import { useEffect } from "react";
import {
Link,
useBlocker as useBlockerCore,
BlockerFunction,
useLocation,
Location,
} from "react-router-dom";
const useBlocker = (
when: boolean | BlockerFunction,
alertOptions?: { message?: string }
) => {
const {
state,
location: nextLocation,
reset,
proceed,
} = useBlockerCore(when);
const currentLocation = useLocation();
const renderPrompt = useCallback(
() =>
state === "blocked" ? (
<div>
{alertOptions?.message || "진짜로 이 페이지에서 나갈거야??"}
<button onClick={reset}>아니</button>
<button onClick={proceed}>응!!</button>
</div>
) : null,
[alertOptions?.message, proceed, reset, state]
);
useEffect(() => {
if (
(typeof when === "boolean" && when) ||
(typeof when === "function" &&
when({
currentLocation,
nextLocation: nextLocation as Location,
historyAction: "PUSH" as never,
}))
) {
window.onbeforeunload = (e) => {
e.preventDefault();
e.returnValue = "";
return null;
};
} else {
window.onbeforeunload = null;
}
return () => {
window.onbeforeunload = null;
};
}, [when, state, currentLocation, nextLocation]);
return { state, reset, proceed, renderPrompt };
};
위와 같이 useBlocker 에 추가적으로 alertConfig를 받아 커스텀 prompt를 표기 해줄 수 있다.

이렇게 조건과 alertConfig를 사용하면 특정 조건에 커스텀한 prompt를 상황에 맞게 표기 해줄 수 있다.
const [value, setValue] = useState("");
const [step, setStep] = useState(1);
const renderMessage = useMemo(() => {
switch (step) {
case 1:
return "첫번째 스텝에서 이탈할거야??";
case 2:
return "두번째 스텝에서 이탈할거야??";
case 3:
return "세번째 스텝에서 이탈할거야??";
default:
return "진짜 이 진행을 멈출거야??";
}
}, [step]);
const { state, renderPrompt } = useBlocker(
({ currentLocation, nextLocation }) => {
// 이동할 path 정보에 따른 선택적 분기 처리도 가능
return !!value && currentLocation?.pathname !== nextLocation?.pathname;
},
{ message: renderMessage }
);
또한 step 형식으로 진행되는 페이지에서 query string을 이용한 step 이동시에 지금 페이지를 제외한 페이지로 이동할때만 작동되도록 할 수도 있어서 편하다.
