BFCache
는 "Back-Forward Cache"의 약자로, 브라우저가 이전에 방문한 페이지의 상태를 캐시해두고 뒤로가기 또는 앞으로 가기 버튼을 눌렀을 때 캐시된 상태를 불러와 페이지를 더 빠르게 로딩하는 기술이다.
페이지를 이동하는 순간 페이지의 전체 스냅숏(자바스크립트 힙 포함)
을 저장해주는 메모리 내부의 캐시를 저장하게 되는데, 사용자가 뒤로가기를 눌러 다시 돌아오려 할 때, 브라우저는 메모리에 저장해둔 전체 페이지를 사용해서 페이지를 빠르고 쉽게 복원해준다.
그러나
BFCache
는 Next.js에서와 같은SPA (Single Page Application)
의 환경에서는 정상적으로 동작하지 않을 수 있다.
SPA
에서는 페이지가 전체적으로 다시 로드되는 것이 아니라 필요한 부분만 업데이트되기 때문이다.
또한, 일반적인 window의 scroll 위치가 아닌
overflow:scroll
속성이 추가된div
의 경우 당연히BFCache
라는 스냅샷이 저장될 수 없다.
따라서, 다음과 같은 문제가 발생할 수 있다.
Todo
아이템들이 나타난다.Todo
아이템을 클릭할 시 상세 페이지로 이동한다.BFCache
를 통해 원래 스크롤 위치로 이동한다.Todo
아이템들이 나타난다.Todo
아이템을 클릭할 시 상세 페이지로 이동한다.
useEffect
를 활용해Custom hook
을 만들자 !
_참고로 Page Router를 기준으로 작성되었다.
App Router의 경우에는 next/navigation
모듈에서 events
객체가 없어졌기 때문에 해당 방법을 사용할 수 없습니다.
해당 이슈
import { useEffect } from 'react';
import Router from 'next/router';
function saveScrollPos(url) {
const scrollPos = { x: window.scrollX, y: window.scrollY };
sessionStorage.setItem(url, JSON.stringify(scrollPos));
}
function restoreScrollPos(url) {
const scrollPos = JSON.parse(sessionStorage.getItem(url));
if (scrollPos) {
window.scrollTo(scrollPos.x, scrollPos.y);
}
}
export default function useScrollRestoration(router) {
useEffect(() => {
if ('scrollRestoration' in window.history) {
let shouldScrollRestore = false;
window.history.scrollRestoration = 'manual';
restoreScrollPos(router.asPath);
const onBeforeUnload = event => {
saveScrollPos(router.asPath);
delete event['returnValue'];
};
const onRouteChangeStart = () => {
saveScrollPos(router.asPath);
};
const onRouteChangeComplete = url => {
if (shouldScrollRestore) {
shouldScrollRestore = false;
restoreScrollPos(url);
}
};
window.addEventListener('beforeunload', onBeforeUnload);
Router.events.on('routeChangeStart', onRouteChangeStart);
Router.events.on('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => {
shouldScrollRestore = true;
return true;
});
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
Router.events.off('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => true);
};
}
}, [router]);
}
useEffect
: 컴포넌트가 마운트될 때 실행된다.window.history.scrollRestoration
: BFCache
와 관련이 있는데, 페이지 전환 시 브라우저에게 수동으로 스크롤 위치를 알려준다.manual
은 자동 복원을 비활성화하고, 수동으로 스크롤 위치를 관리하겠다는 의미이다.restoreScrollPos
: sessionStorage를 사용하여 현재 페이지의 스크롤 위치를 저장하고, 이를 복원하는 함수onBeforeUnload
이벤트 핸들러: 페이지가 언로드되기 전에 호출되는 이벤트로, 현재 페이지의 스크롤 위치를 저장한다.onRouteChangeStart
이벤트 핸들러: 페이지 전환이 시작되기 전에 호출되는 이벤트로, 현재 페이지의 스크롤 위치를 저장onRouteChangeComplete
이벤트 핸들러: 페이지 전환이 완료된 후 호출되는 이벤트로, 이전 페이지로 돌아왔을 때 스크롤 위치를 복원한다.Router.beforePopState
: 뒤로가기 또는 앞으로 가기 버튼을 클릭할 때 발생하는 이벤트로, 페이지를 이동하기 전에 스크롤 위치를 복원하기 위해 사용한다.이제 이 커스텀 훅을 App.tsx
에 적용하면 된다.
import useScrollRestoration from "utils/hooks/useScrollRestoration";
const App = ({ Component, pageProps, router }) => {
useScrollRestoration(router);
return <Component {...pageProps} />;
};
export default App;
그런데 만약에 window
가 아닌 특정 div
의 scroll을 유지하려고 하려면 어떻게 할까?
useRef
를 활용해보자.
import { useEffect, useRef } from 'react';
import Router from 'next/router';
function useScrollRestoration(router) {
const scrollableDivRef = useRef(null);
const saveScrollPos = (url) => {
const scrollableDiv = scrollableDivRef.current;
if (scrollableDiv) {
const scrollPos = { x: scrollableDiv.scrollLeft, y: scrollableDiv.scrollTop };
sessionStorage.setItem(url, JSON.stringify(scrollPos));
}
};
const restoreScrollPos = (url) => {
const scrollableDiv = scrollableDivRef.current;
if (scrollableDiv) {
const scrollPos = JSON.parse(sessionStorage.getItem(url));
if (scrollPos) {
scrollableDiv.scrollLeft = scrollPos.x;
scrollableDiv.scrollTop = scrollPos.y;
}
}
};
useEffect(() => {
if ('scrollRestoration' in window.history) {
let shouldScrollRestore = false;
window.history.scrollRestoration = 'manual';
restoreScrollPos(router.asPath);
const onBeforeUnload = (event) => {
saveScrollPos(router.asPath);
delete event['returnValue'];
};
const onRouteChangeStart = () => {
saveScrollPos(router.asPath);
};
const onRouteChangeComplete = (url) => {
if (shouldScrollRestore) {
shouldScrollRestore = false;
restoreScrollPos(url);
}
};
window.addEventListener('beforeunload', onBeforeUnload);
Router.events.on('routeChangeStart', onRouteChangeStart);
Router.events.on('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => {
shouldScrollRestore = true;
return true;
});
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
Router.events.off('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => true);
};
}
}, [router]);
return scrollableDivRef;
}
export default useScrollRestoration;
바로 다음 글에서 최신 방법에 맞게 Page Router를 사용하지 않고
App Router에 맞게 끔 코드를 수정한 후 적용해보자 !
그 후 useEffect
와 useLayoutEffect
를 비교해보고 왜 더 나은 방법을 제공해 줄 수 있는지 공유하도록 하겠다 ..!!
이어서 계속..