[Next.js] ๋ผ์ฐํธ ๋ณ๊ฒฝ / ์๋ก๊ณ ์นจ ์ทจ์ํ๊ธฐ (๋ค๋น๊ฒ์ด์ ๊ฐ๋)
ํ์๊ฐ์ , ๊ธ์ฐ๊ธฐ ๋ฑ ์ ๋ ฅ Form ํ์ด์ง์์ ์ค์๋ก ๋ค๋ฅธ ๋งํฌ๋ฅผ ํด๋ฆญํ๊ฑฐ๋, ์ ์ฅํ๋ ๊ฒ์ ๊น๋นกํ๊ณ ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๋ฉด ์ ์ ์ ์ฅ์์ ๋ฌด์ฒ ์ง์ฆ๋๋ ์ํฉ์ด ๋๋ค. ์ฒ์๋ถํฐ ํผ์ ๋ค์ ์์ฑํ๊ฑฐ๋ ์์ ํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค. ์์ ์ ์ฅ ๊ธฐ๋ฅ์ด ์๋ค๋ฉด ๊ด์ฐฎ์ง๋ง, ๊ทธ๋ ์ง ์๋ค๋ฉด ํ์ด์ง ์ดํ์ ๋ํ Confirm ๋จ๊ณ๋ฅผ ์ถ๊ฐํด์ ์ฌ์ฉ์ฑ์ ๊ฐ์ ํ ์ ์๋ค. ์ค์ ๋ก ์ฌ๋ฌ ์น ์๋น์ค์์ Form ํ์ด์ง ์ดํ์ ‘์ ์ฅํ์ง ์์ ๋ด์ฉ์ ์ญ์ ๋๋ค’๋ ์๋ด ๋ฌธ๊ตฌ๋ฅผ ๋์ด๋ค.
๊ตฌํ ๋ฐฉ๋ฒ
NextJS ์์ฒด์ ์ผ๋ก ์ฌ๋ฌ ๋ผ์ฐํธ ์ด๋ฒคํธ๋ฅผ ์ ๊ณตํ๋๋ฐ routeChangeStart๋ ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์์ํ ๋ ํธ๋ฆฌ๊ฑฐ๋๋ ์ด๋ฒคํธ๋ค. ํ์ด์ง๋ฅผ ์ธ๋ก๋(์๋ก๊ณ ์นจ)ํ ๋ window ๊ฐ์ฒด์์ ๋ฐ์ํ๋ beforeunload ์ด๋ฒคํธ๋ฅผ ์ด์ฉํ๋ฉด ๋๋ค. ๋๋ต ์๋ 4๊ฐ์ง ๋จ๊ณ๋ฅผ ํตํด ๊ตฌํํ๋ค.
๋จ๊ณ๋ณ ๊ณผ์
- ์ ์ ๊ฐ Form์ ์ ์ฅํ์ง ์์ ์ํ์์ ๋ค๋ฅธ ํ์ด์ง ์ด๋(๋ผ์ฐํธ ๋ณ๊ฒฝ) ์๋
a. โ ๏ธ Prev/Next ๋ฒํผ์ ๋๋ฅธ ์์ ์ ์ด๋ํ ํ์ด์ง ์ฃผ์๋ก ์ฃผ์์ฐฝ ๊ฒฝ๋ก๊ฐ ๋ฐ๋๋ค
b. ๐ ๋๋ฌธ์ ๋ผ์ฐํธ ๋ณ๊ฒฝ ์ทจ์๋ฅผ ๋๋นํด ํ์ฌ ๊ฒฝ๋ก๋ฅผ ์์ ์ ์ฅํด๋๋ค routeChangeStart์ด๋ฒคํธ ํธ๋ค๋ฌ ์คํ- ์ ์ ์ ์ ํ์ ๋ฐ๋ผ ์๋ ๊ฐ์ ๋ฐํํ๋ callback ์คํ
a. ํ์ธtrue: ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋(ํ์ด์ง ์ดํ)
b. ์ทจ์false: ํ์ด์ง ์ด๋ ์ทจ์ - ์ ์ ๊ฐ ์ทจ์ ๋ฒํผ์ ๋๋ฌ ํ์ฌ ํ์ด์ง์ ๋จธ๋ฌผ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค๋ฉด…
a.1-b์์ ์์ ์ ์ฅํ ๊ฒฝ๋ก๋ก ๋ณต๊ตฌ —router.replace(...)
b.routeChangeError์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ์ ๋ผ์ฐํธ ๋ณ๊ฒฝ ์ทจ์
์ฃผ์ํ ์
์์ ์ ์ฅํ ๊ฒฝ๋ก๋ก router.replace๋ฅผ ์คํํ๋ฉด(4-a) ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์๋ํ๋ฏ๋ก routeChangeStart ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ค. ๊ทธ๋ผ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ์คํ๋ผ์ ์ ๊ณผ์ ์ ๋ ๋ค์ ๋ฐ๋ณตํ๋ฉฐ ๋ฌดํ ๋ ๋๋ง๋๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๋ ค๋ฉด ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์ทจ์ํ์ ๋๋ง ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์คํํ๋๋ก ํด์ผ ํ๋ค. ์ด๋ฅผ ์ํด isKilledRouter ๊ฐ์ ๋ด๋ถ ์ํ๋ก ๋ผ์ฐํธ ๋ณ๊ฒฝ ์ทจ์ ์ฌ๋ถ๋ฅผ ๊ด๋ฆฌํ๋ค.
isKilledRouter ๊ฐ (๋ผ์ฐํธ ๋ณ๊ฒฝ ์ทจ์ ์ฌ๋ถ) |
routeChangeStart ํธ๋ค๋ฌ ์คํ ์ฌ๋ถ |
true |
โ |
false |
โ |
์ ์ ๊ฐ ํ์ด์ง ์ด๋์ ์ทจ์ํ ํ ๋ค์ ํ์ด์ง ์ด๋์ ์๋ํ๋ ์ํฉ๋ ๊ณ ๋ คํด์ผ ํ๋ค. ์ฆ, 1~4๋ฒ ๋จ๊ณ๊น์ง ํ ์ฌ์ดํด ์คํ์ ๋ง์น ๋ค์๋ ๋ค์ ์ฌ์ดํด์์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ์คํ๋ ์ ์๋๋ก isKilledRouter ์ํ๋ฅผ false๋ก ๋ฐ๊ฟ์ฃผ๋ ์์
์ด ํ์ํ๋ค.
push, replace ์ฐจ์ด์ ๋น๊ต
๐ก router.replace๋ history stack์ ์๋ ๊ฐ์ฅ ๋ง์ง๋ง ์์๋ฅผ ์ด๋ํ ๊ฒฝ๋ก๋ก ๋์ฒดํ๋ค. ์ฆ ํ์ฌ ํ์ด์ง๋ฅผ ์๋ก์ด ํ์ด์ง๋ก ๋ฎ์ด์ด๋ค. ๊ทธ ์ธ์ router.push์ ๋์ผํ๋ค. — ์ฐธ๊ณ ํฌ์คํ
push์ด์ฉ์- ์ด๋ :
'/'→'/profiles'→'/profiles/smith' - ๋ง์ง๋ง ํ์ด์ง์์ ๋ค๋ก๊ฐ๊ธฐ ํ :
'profiles' - history stack:
['/', 'profiles', 'profiles/smith']
- ์ด๋ :
replace์ด์ฉ์- ์ด๋ :
'/'(push) →'/profiles'(replace) →'/profiles/smith' - ๋ง์ง๋ง ํ์ด์ง์์ ๋ค๋ก๊ฐ๊ธฐ ํ :
'/' - history stack:
['/', 'profiles/smith']
- ์ด๋ :
๊ตฌํ ํ๊ธฐ
์ฐธ๊ณ ๋ด์ฉ
router.events๋ Router ์์ฑ์ ๋ฐuseRouterํ ์ด ๋ฐํํ๋ ์ธ์คํด์ค๋ก ์ ๊ทผํ ์ ์๋ค.thenํ์ ๋ฉ์๋์ ์ฝ๋ฐฑ์ ๋ง์ดํฌ๋กํ์คํฌํ์ ๋ค์ด๊ฐ๋ค๊ฐ ์ฝ์คํ์ด ๋น์์ ๋ ์คํ๋๋ค- NextJS์์ ๋ผ์ฐํ
์ด ๋ฐ์ํ๋ฉด ๋ฐ์ดํฐ ํจ์นญ ๋ฉ์๋(
get...Props)๊ฐ ์คํ๋๋ค. ๋ฐ์ดํฐ ํจ์นญ ๋ฉ์๋๋ฅผ ์คํํ์ง ์๊ณ URL์ ๋ณ๊ฒฝํ๊ณ ์ถ์ ๋shallow: true์ต์ ์ ๋๊ธด๋ค. — ์ฐธ๊ณ ๊ธ - next/router๋ Context API๋ฅผ ์ฌ์ฉํ๋ค.
router.*๋ฉ์๋๋ฅผ ์คํํ๋ฉด ๋ด๋ถ ์ํ ๊ฐ์ ๋ณ๊ฒฝํ๋ฏ๋ก ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ค.window.history.replaceState๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฆฌ์กํธ์ ์๊ด์๋ ํ์คํ ๋ฆฌ ๊ฐ์ฒด๋ฅผ ํธ๋ค๋งํ๊ธฐ ๋๋ฌธ์ ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ์ง ์๋๋ค. — ์ฐธ๊ณ ๊ธ
router.replace ์ฌ์ฉ
์ปค์คํ
ํ
์ด ๋ฐ๋ shouldStopNavigation ํ๋ผ๋ฏธํฐ๊ฐ true๋ผ๋ฉด routeChangeStart ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๋ฑ๋ก๋๋ค. ๊ทธ ํ ์ ์ ๊ฐ ํ์ด์ง ์ด๋(๋ผ์ฐํธ ๋ณ๊ฒฝ)์ ์๋ํ๋ฉด onRouteChange ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์คํํ๋ค.
โถ onRouteChange() ์ด๋ฒคํธ ํธ๋ค๋ฌ ์คํ — ํ์ด์ง ์ด๋(์ ์ ์ก์
์ผ๋ก ์ธํด)
- ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์ทจ์ํ ์ ์ด ์์ผ๋ฏ๋ก
if์กฐ๊ฑด ํต๊ณผ ํcallbackํจ์ ์คํ - ์ ์ ๊ฐ ์ทจ์ ๋ฒํผ์ ๋๋ฌ ํ์ด์ง์ ๋จธ๋ฌด๋ฅด๊ธฐ๋ก ํ๋ค๋ฉด
if(!confirmed)์กฐ๊ฑด๋ฌธ ์คํ(2๋ฒ~)
โท recoverToCurrentPath() ํจ์ ์คํ
router.replace(...)์คํthen๋ฉ์๋ ํธ๋ค๋ฌ๋ ๋ง์ดํฌ๋กํ์คํฌ ํ์์ ๋๊ธฐ
โธ killRouterEvent() ํจ์ ์คํ
isKilledRouter์ํtrue๋ก ๋ณ๊ฒฝrouteChangeError์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ํด
โน onRouteChange() ์ด๋ฒคํธ ํธ๋ค๋ฌ ์คํ — currentPath ๋ณต๊ตฌ
- 2๋ฒ์์ ๋ผ์ฐํธ ๋ณ๊ฒฝ(
router.replace)์ ์๋ํ์ผ๋ฏ๋ก ํธ๋ค๋ฌ๊ฐ ์คํ๋จ - 3๋ฒ์์
isKilledRouter์ํ๋ฅผ ๋ณ๊ฒฝํ์ผ๋ฏ๋กif์กฐ๊ฑด ํต๊ณผ ๋ชปํ๊ณ ์ข ๋ฃ
๐ โท currentPath ๋ณต๊ตฌ์ ๋ํ ๋ผ์ฐํธ ๋ณ๊ฒฝ ์๋ฃ
๐ โด ํ์ด์ง ์ด๋์ ๋ํ โถ ๋ผ์ฐํธ ๋ณ๊ฒฝ ์๋ฌ ๋ฐ์ ๋ฐ clean-up & effect ์คํ
โบ then() ๋ฉ์๋ ํธ๋ค๋ฌ ์คํ
isKilledRouter์ํfalse๋ก ๋ณ๊ฒฝ — ๋ค์ ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ ์คํ์ ์ํดthenํธ๋ค๋ฌ์res์ธ์๋replace์ฑ๊ณต ์ฌ๋ถ๋ฅผ ๋ํ๋ด๋ boolean ๊ฐ ๋ฐํ
๐ก /profile/edit ํ์ด์ง์์ ๋ค๋ก๊ฐ๊ธฐ(/profile) ํ๋ค๊ณ ๊ฐ์ ํ๊ณ ์ฝ์ ์ถ๋ ฅ ๋ด์ฉ ์์ฑ
// ์ ํธ๋ฆฌํฐ ํจ์
const getWindow = () => (typeof window !== 'undefined' ? window : null);
// useNavigationInterceptor.ts
const useNavigationInterceptor = (
shouldStopNavigation: boolean, // ๋ผ์ฐํธ ๋ณ๊ฒฝ ๊ด๋ จ ์ด๋ฒคํธ ๊ฐ์ง ์ฌ๋ถ(Hook ํ์ฑ ์ฌ๋ถ)
callback: () => boolean, // *ex) () => confirm('...')*
) => {
const router = useRouter();
const currentPath = useRef(router.asPath); // ํ์ฌ ๊ฒฝ๋ก ์ ์ฅ
const isKilledRouter = useRef(false); // ๋ผ์ฐํธ ๋ณ๊ฒฝ ์ทจ์ ์ฌ๋ถ
const killRouterEvent = useCallback(() => {
console.log(`killRouterEvent | path: ${location.pathname}`); // โถ /profile
isKilledRouter.current = true;
Router.events.emit('routeChangeError'); // ๋ผ์ฐํธ ๋ณ๊ฒฝ ์๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํด
throw 'Abort route change. You can ignore this error'; // ์ฝ์์ ์๋ฌ ์ถ๋ ฅ
}, []);
const recoverToCurrentPath = useCallback(() => {
console.log(`beforeReplace | path: ${location.pathname}`); // โต /profile
router
.replace(currentPath.current, undefined, { shallow: true })
.then((res: boolean) => {
// replace ์์
์ฑ๊ณต ์ฌ๋ถ
isKilledRouter.current = false;
console.log(`afterReplace | path: ${location.pathname}`); // โธ /profile/edit (๊ฒฝ๋ก ๋ณต๊ตฌ๋จ)
});
}, [router]);
// ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์คํ๋ ํธ๋ค๋ฌ
const onRouteChangeStart = useCallback(
(toUrl: string) => {
// ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์์ํ๋ ์์ ์ ์ด๋ํ ๊ฒฝ๋ก๊ฐ ์ฃผ์์ฐฝ์ ๋ฏธ๋ฆฌ ๋ฐ์๋จ
console.log(`onRouteChange | path: ${location.pathname}`); // โด /profile, โท /profile
if (isKilledRouter.current) return;
const confirmed = callback(); // Confirm ์ฐฝ ์ ํ ๊ฒฐ๊ณผ
if (!confirmed) {
recoverToCurrentPath();
killRouterEvent();
}
},
[callback, killRouterEvent, recoverToCurrentPath],
);
// ์๋ก๊ณ ์นจ์ ์คํ๋ ํธ๋ค๋ฌ
const onBeforeUnload = useCallback((e: BeforeUnloadEvent) => {
e.preventDefault(); // HTML ํ์ค ๋ช
์ธ ์ค์(unload ์ด๋ฒคํธ ์ทจ์)
e.returnValue = ''; // for Chrome
}, []);
useEffect(() => {
if (shouldStopNavigation) {
console.log(`effect | path: ${location.pathname}`); // (4-3) /profile/edit
getWindow()?.addEventListener('beforeunload', onBeforeUnload); // ์๋ก๊ณ ์นจ
Router.events.on('routeChangeStart', onRouteChangeStart); // ํ์ด์ง ์ด๋
return () => {
console.log(`cleanup | path: ${location.pathname}`); // (4-2) /profile/edit
getWindow()?.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
};
}
}, [onBeforeUnload, onRouteChangeStart, shouldStopNavigation]);
};
// useNavigationInterceptor.ts ํ
์ฌ์ฉ
// isDirty๋ react-hook-form์ formState๊ฐ ๋ฐํํ๋ ์ํ๋ค.
// ํ๋๋ฅผ ์์ ํ๋ค๋ฉด isDirty ๊ฐ์ true๊ฐ ๋๋ค
useNavigationInterceptor(isDirty, () => {
return confirm('Warning! You have unsaved changes');
});
๋ฆฌํฉํ ๋ง — history.replaceState ์ฌ์ฉ โญ๏ธ
currentPath๋ฅผ ๋ณต๊ตฌํ ๋ router.replace() ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ๋ฆฌ๋ ๋๋ง์ ํธ๋ฆฌ๊ฑฐํ๋ฏ๋ก effect ํจ์๊ฐ ์คํ๋๋ค. ๋ํ onRouteChange ์ด๋ฒคํธ ๋ฐ์์ผ๋ก ํธ๋ค๋ฌ ์ค๋ณต ํธ์ถ์ ๋ฐฉ์งํ๊ธฐ ์ํด isKilledRouter ๊ฐ์ ์ถ๊ฐ์ ์ธ ๋ด๋ถ ์ํ๋ ํ์ํ๋ค.
History API์ replaceState() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์กฐ๊ธ ๋ ์ต์ ํํ ์ ์๋ค. replaceState() ๋ฉ์๋๋ ๋ฆฌ์กํธ์ ์๊ด์๋ ํ์คํ ๋ฆฌ ๊ฐ์ฒด๋ฅผ ํธ๋ค๋งํ๋ฏ๋ก ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ์ง ์๋๋ค. ๋ํ popstate ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๊ธฐ ๋๋ฌธ์ onRouteChange ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ํธ์ถํ์ง ์๋๋ค. ๊ทธ๋ผ isKilledRouter ์ํ๋ ํ์ ์์ด์ง๋ค.
addEventListener('popstate', (event) => {
/* replaceState() ๋ฉ์๋ ํธ์ถ์ ์คํ */
});
ํํธ currentPath์๋ router.asPath ๊ฐ์ ์ ์ฅํ๊ณ ์๋ค. asPath๋ locale๊ณผ basePath๋ฅผ ํฌํจํ์ง ์์ง๋ง, router.replace()๋ฅผ ์ฌ์ฉํ๋ฉด locale์ ๊ทธ๋๋ก ์ ์งํ๋ฏ๋ก ํฐ ๋ฌธ์ ๊ฐ ์๋ค.
๋ฐ๋ฉด replaceState()๋ฅผ ์ฌ์ฉํ๋ฉด asPath ๊ฐ์ด ๊ทธ๋๋ก ์ฃผ์์ฐฝ์ ๋ค์ด๊ฐ๋ฏ๋ก locale์ ํฌํจํ์ง ์๊ฒ ๋๋ค. asPath ๋์ location.pathname์ ์ฌ์ฉํ๋ฉด locale์ ํฌํจํ ์ฃผ์์ฐฝ์ผ๋ก ๋ณ๊ฒฝ๋๋๋ก ํ ์ ์๋ค.
// useNavigationInterceptor.ts ๊ฐ์ ์ฝ๋
// (์ ๊ฑฐ) const isKilledRouter = useRef(false);
const currentPath = useRef(router.asPath); // router.asPath ๊ฐ์ผ๋ก ์์ ํ ๋น
const killRouterEvent = useCallback(() => {
// (์ ๊ฑฐ) isKilledRouter.current = true;
Router.events.emit('routeChangeError');
throw 'Abort route change. You can ignore this error';
}, []);
const recoverToCurrentPath = useCallback(() => {
// (๊ต์ฒด) router.replace(...)
getWindow()?.history.replaceState(null, '', currentPath.current);
}, []);
const onRouteChangeStart = useCallback(
(toUrl: string) => {
// (์ ๊ฑฐ) if (isKilledRouter.current) return;
const confirmed = callback();
if (!confirmed) {
recoverToCurrentPath();
killRouterEvent();
}
},
[callback, killRouterEvent, recoverToCurrentPath],
);
// (์ถ๊ฐ) useEffect๋ ํ๋ฉด์ ํ์ธํ
์ ๋ง์น ํ ์คํ๋๋ฏ๋ก window ๊ฐ์ฒด์ ์ ๊ทผํ ์ ์๋ ์์
useEffect(() => {
const window = getWindow();
if (window) currentPath.current = window.location.pathname;
}, []);
// useNavigationInterceptor.ts
// ์ ํธ๋ฆฌํฐ ํจ์
const getWindow = () => (typeof window !== 'undefined' ? window : null);
const useNavigationInterceptor = (
shouldStopNavigation: boolean, // ๋ผ์ฐํธ ๋ณ๊ฒฝ ๊ด๋ จ ์ด๋ฒคํธ ๊ฐ์ง ์ฌ๋ถ(Hook ํ์ฑ ์ฌ๋ถ)
callback: () => boolean, // ex) () => confirm('...')
) => {
const router = useRouter();
const currentPath = useRef(router.asPath); // router.asPath ๊ฐ์ผ๋ก ์์ ํ ๋น
const killRouterEvent = useCallback(() => {
console.log(`killRouterEvent | path: ${location.pathname}`); // โถ /profile/edit
Router.events.emit('routeChangeError'); // ๋ผ์ฐํธ ๋ณ๊ฒฝ ์๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํด
throw 'Abort route change. You can ignore this error'; // ์ฝ์์ ์๋ฌ ์ถ๋ ฅ
}, []);
const recoverToCurrentPath = useCallback(() => {
getWindow()?.history.replaceState(null, '', currentPath.current);
console.log(`beforeReplace | path: ${location.pathname}`); // โต /profile/edit
}, []);
// ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์คํ๋ ํธ๋ค๋ฌ
const onRouteChangeStart = useCallback(
(toUrl: string) => {
// ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์์ํ๋ ์์ ์ ์ด๋ํ ๊ฒฝ๋ก๊ฐ ์ฃผ์์ฐฝ์ ๋ฏธ๋ฆฌ ๋ฐ์๋จ
console.log(`onRouteChange | path: ${location.pathname}`); // โด /profile
const confirmed = callback(); // Confirm ์ฐฝ ์ ํ ๊ฒฐ๊ณผ
if (!confirmed) {
recoverToCurrentPath();
killRouterEvent();
}
},
[callback, killRouterEvent, recoverToCurrentPath],
);
// ์๋ก๊ณ ์นจ์ ์คํ๋ ํธ๋ค๋ฌ
const onBeforeUnload = useCallback((e: BeforeUnloadEvent) => {
e.preventDefault(); // HTML ํ์ค ๋ช
์ธ ์ค์(unload ์ด๋ฒคํธ ์ทจ์)
e.returnValue = ''; // for Chrome
}, []);
useEffect(() => {
if (shouldStopNavigation) {
getWindow()?.addEventListener('beforeunload', onBeforeUnload); // ์๋ก๊ณ ์นจ
Router.events.on('routeChangeStart', onRouteChangeStart); // ํ์ด์ง ์ด๋
return () => {
getWindow()?.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
};
}
}, [onBeforeUnload, onRouteChangeStart, shouldStopNavigation]);
useEffect(() => {
const window = getWindow();
if (window) currentPath.current = window.location.pathname;
}, []);
};
์์ฒ๋ผ ์ฝ๋๋ฅผ ๊ฐ์ ํ๊ณ ๋ผ์ฐํธ ๋ณ๊ฒฝ / ์๋ก๊ณ ์นจ์ ํด๋ณด๋ฉด ์๋ 3๊ฐ ๋ฉ์๋๋ง ์คํ๋๋ค. (/profile/edit ํ์ด์ง์์ ํผ ์์ ํ ์ ์ฅํ์ง ์๊ณ ๋ค๋ฅธ ํ์ด์ง ์ด๋์ ์๋ํ๋ค๊ณ ๊ฐ์ )
onRouteChangeStart—/profile/edit→/profilerecoverToCurrentPath—/profile→/profile/editkillRouterEvent—/profile/edit
Confirm ํ์ธ์ ๋๋ ์ ๋๋ง ์ฃผ์์ฐฝ ๋ณ๊ฒฝ
๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์์ํ๋ฉด ์ด๋ํ ๊ฒฝ๋ก๊ฐ ์ฃผ์์ฐฝ์ ๋ฏธ๋ฆฌ ๋ฐ์๋๋ค. ํ์ฌ๋ Confirm ์ฐฝ ์ทจ์๋ฅผ ๋๋ฅด๋ฉด ํ์ฌ ํ์ด์ง ๊ฒฝ๋ก๋ก ๋๋๋ฆฌ๊ณ (์ด๋ ์ฃผ์์ฐฝ์ด ๋ฐ๋๋๊ฑธ ์ ์ ๊ฐ ํ์ธํ ์ ์๋ค), ํ์ธ์ ๋๋ฅด๋ฉด ๋ฏธ๋ฆฌ ๋ฐ์๋ ๊ฒฝ๋ก๊ฐ ์ ์ง๋๋ค.
๋ง์ฝ Confirm ์ฐฝ ์ทจ์ ๋ฒํผ์ ๋๋ ์๋ ์ฃผ์์ฐฝ์ ์๋ฌด ๋ณํ๊ฐ ์๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ณ , ํ์ธ์ ๋๋ ์ ๋๋ง ์ฃผ์์ฐฝ์ด ์ด๋ํ ๊ฒฝ๋ก๋ก ๋ฐ๋๋๋ก ํ๊ณ ์ถ๋ค๋ฉด, replaceState ์คํ ์์๋ฅผ ์ด์ง ๋ฐ๊ฟ์ฃผ๋ฉด ๋๋ค.
๐ก routeChangeStart ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ โถurl โท{ shallow }๋ฅผ ์ธ์๋ก ๋ฐ๋๋ค. url์ ์ด๋ํ ๊ฒฝ๋ก๋ฅผ ๋ํ๋ธ๋ค — ์ ํํ ๋งํ๋ฉด ํ์ฌ ๋ธ๋ผ์ฐ์ ์ ํ์๋๊ณ ์๋ baseUrl์ ํฌํจํ ์ฃผ์.
// useNavigationInterceptor.ts
// recoverToCurrentPath -> changePath๋ก ์ด๋ฆ ๋ณ๊ฒฝ
const changePath = useCallback((url: string) => {
getWindow()?.history.replaceState(null, '', url);
}, []);
const onRouteChangeStart = useCallback(
(toUrl: string) => {
// ์ด๋ํ ๊ฒฝ๋ก๊ฐ ์ฃผ์์ฐฝ์ ๋ฏธ๋ฆฌ ๋ฐ์๋จ (์ด๋ํ ๊ฒฝ๋ก === toUrl)
changePath(currentPath.current); // ํ์ฌ ํ์ด์ง ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ
const confirmed = callback(); // Confirm ์ฐฝ ์ ํ ๊ฒฐ๊ณผ
if (!confirmed) killRouterEvent();
else changePath(toUrl); // ํ์ธ ๋ฒํผ์ ๋๋ ์ ๋๋ง ์ด๋ํ ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ
},
[callback, changePath, killRouterEvent],
);
์ต์ข ์ฝ๋ โก๏ธ
// useNavigationInterceptor.ts
// ์ ํธ๋ฆฌํฐ ํจ์
const getWindow = () => (typeof window !== 'undefined' ? window : null);
const useNavigationInterceptor = (
shouldStopNavigation: boolean, // ๋ผ์ฐํธ ๋ณ๊ฒฝ ๊ด๋ จ ์ด๋ฒคํธ ๊ฐ์ง ์ฌ๋ถ(Hook ํ์ฑ ์ฌ๋ถ)
callback: () => boolean, // ex) () => confirm('...')
) => {
const router = useRouter();
const currentPath = useRef(router.asPath); // router.asPath ๊ฐ์ผ๋ก ์์ ํ ๋น
const killRouterEvent = useCallback(() => {
Router.events.emit('routeChangeError'); // ๋ผ์ฐํธ ๋ณ๊ฒฝ ์๋ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํด
throw 'Abort route change. You can ignore this error'; // ์ฝ์์ ์๋ฌ ์ถ๋ ฅ
}, []);
const changePath = useCallback((url: string) => {
getWindow()?.history.replaceState(null, '', url);
}, []);
// ๋ผ์ฐํธ ๋ณ๊ฒฝ์ ์คํ๋ ํธ๋ค๋ฌ
const onRouteChangeStart = useCallback(
(toUrl: string) => {
// ์ด๋ํ ๊ฒฝ๋ก๊ฐ ์ฃผ์์ฐฝ์ ๋ฏธ๋ฆฌ ๋ฐ์๋จ (์ด๋ํ ๊ฒฝ๋ก === toUrl)
changePath(currentPath.current); // ํ์ฌ ํ์ด์ง ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ
const confirmed = callback(); // Confirm ์ฐฝ ์ ํ ๊ฒฐ๊ณผ
if (!confirmed) killRouterEvent();
else changePath(toUrl); // ํ์ธ ๋ฒํผ์ ๋๋ ์ ๋๋ง ์ด๋ํ ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ
},
[callback, changePath, killRouterEvent],
);
// ์๋ก๊ณ ์นจ์ ์คํ๋ ํธ๋ค๋ฌ
const onBeforeUnload = useCallback((e: BeforeUnloadEvent) => {
e.preventDefault(); // HTML ํ์ค ๋ช
์ธ ์ค์(unload ์ด๋ฒคํธ ์ทจ์)
e.returnValue = ''; // for Chrome
}, []);
useEffect(() => {
if (shouldStopNavigation) {
getWindow()?.addEventListener('beforeunload', onBeforeUnload); // ์๋ก๊ณ ์นจ
Router.events.on('routeChangeStart', onRouteChangeStart); // ํ์ด์ง ์ด๋
return () => {
getWindow()?.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
};
}
}, [onBeforeUnload, onRouteChangeStart, shouldStopNavigation]);
useEffect(() => {
const window = getWindow();
if (window) currentPath.current = window.location.pathname;
}, []);
};
๋ ํผ๋ฐ์ค
- Window: beforeunload ์ด๋ฒคํธ - Web API | MDN
- DOMContentLoaded, load, beforeunload, unload ์ด๋ฒคํธ
- Route cancellation · Discussion #32231 · vercel/next.js
- Next JS: Warn User for Unsaved Form before Route Change
- nextjs๋ฅผ ์ ์ฉํ๋ฉด์ ์๊ฒ ๋ ์ฌ์ค๋ค
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[JS] Promise ํ๋ก๋ฏธ์ค ๋ณ๋ ฌ์ฒ๋ฆฌ ๋ฉ์๋ ํบ์๋ณด๊ธฐ
[JS] Promise ํ๋ก๋ฏธ์ค ๋ณ๋ ฌ์ฒ๋ฆฌ ๋ฉ์๋ ํบ์๋ณด๊ธฐ
2024.05.14 -
[JS] ํ์ ์ด๋ฆ์ ๋ฐํํ๋ getType ์ ํธ ํจ์
[JS] ํ์ ์ด๋ฆ์ ๋ฐํํ๋ getType ์ ํธ ํจ์
2024.05.14 -
[JS] API ์์ฒญ / ๋น๋๊ธฐ ์์ ์ทจ์ํ๊ธฐ - Abort Controller
[JS] API ์์ฒญ / ๋น๋๊ธฐ ์์ ์ทจ์ํ๊ธฐ - Abort Controller
2024.05.14 -
[Next.js] Next/Image base64 placeholder ๋ง๋ค๊ธฐ (๋ธ๋ฌ ์ฒ๋ฆฌ๋ ํ๋ ์ด์คํ๋)
[Next.js] Next/Image base64 placeholder ๋ง๋ค๊ธฐ (๋ธ๋ฌ ์ฒ๋ฆฌ๋ ํ๋ ์ด์คํ๋)
2024.05.14