무한 스크롤 등으로 구현된 목록 페이지에서 아이템을 클릭하여 페이지에 들어갔다 나오는 경우 페이지의 스크롤 위치를 기억하는 것은 좋은 사용자 경험을 주기 위한 기능이다. 개인 프로젝트 쿠키지에도 유용하게 사용하였다. NextJS에서 해당 기능을 구현하기 위한 훅스를 알아보자.
import { useLayoutEffect } from "react";
import { usePathname } from "next/navigation";
export function useScrollRestorationCSR() {
const pathname = usePathname();
useLayoutEffect(() => {
let curScrollY = 0;
const scrollHandler = () => {
curScrollY = window.scrollY;
};
const scrollResetHandler = () => {
sessionStorage.setItem(`scrollY:${pathname}`, "0");
};
const storageScrollY = Number(
sessionStorage.getItem(`scrollY:${pathname}`)
);
if (storageScrollY) window.scrollTo(0, storageScrollY);
window.addEventListener("scroll", scrollHandler);
window.addEventListener("unload", scrollResetHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
window.removeEventListener("unload", scrollResetHandler);
if (curScrollY !== 0)
sessionStorage.setItem(`scrollY:${pathname}`, curScrollY.toString());
};
}, [pathname]);
}
윈도우 스크롤 이벤트를 통해 현재의 스크롤 위치를 최신화 시킨다. 다른 페이지로 이동했을 때, 클린업 함수를 사용해서 세션 스토리지에 현재의 스크롤 위치를 저장한다. 최종적으로, 목록 페이지에 돌아오게 되면 세션 스토리지에 저장된 스크롤 위치를 꺼내 즉시 해당 스크롤 위치로 이동한다.
해당 훅은 CSR에서 잘 작동한다. SSR에서 사용하게 되면 서버 측의 window.scrollY 값이 0으로 나오게 되어 기능이 잘 작동하지 않는다.
브라우저 저장소를 사용하거나 전역 상태를 사용하여 스크롤 위치를 저장하는 방법이 있다. 전역 상태를 사용하면 새로고침 할 때, 전역 상태 값이 기본값으로 돌아가므로 unload 이벤트를 사용할 필요가 없다는 장점이 있다.
하지만 해당 훅은 재사용 훅이기 때문에, zustand 같은 라이브러리의 사용을 유도하기 때문에 해당 훅만 사용하여 기능을 얻기 위해 전역 상태보다 세션 스토리지를 사용하였다.
스크롤 유지는 데이터 목록을 패칭할 때 많이 사용하게 되는데, 데이터 목록이 렌더링 되기 전의 스크롤이 없는 상태의 페이지에서 이동하기 때문에 기능이 잘 작동하지 않는다. 따라서 화면에 그려진 후에 코드를 실행시키는 useLayoutEffect를 사용하여 스크롤 이동 시점을 올바르게 적용시켰다.
기존 스크롤 위치를 세션 스토리지에 저장하기 때문에 새로고침 하는 경우, 스크롤 위치 데이터가 그대로 남아있다. 따라서 unload 됐을 때 세션 스토리지의 스크롤 값을 0으로 해줘야 한다.
그대로 남아있는 것은 유용하지 않을까 생각될 수도 있지만, 무한 스크롤의 경우 새로고침 할 때, 첫 페이지만 패칭한 상태이기 때문에 기존 위치 유지를 보장할 수 없다.
기존에는 단순히 스크롤 이벤트 없이 클린업 함수에 window.scrollY를 바로 세션 스토리지에 저장하는 방식으로 구현했지만, 스크롤 위치가 정확하지 않은 오류가 발생했다. 따라서 스크롤 이벤트로 실시간 스크롤 위치를 계속 저장하여 해당 문제를 해결했다.