[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
→/profile
recoverToCurrentPath
—/profile
→/profile/edit
killRouterEvent
—/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