Next.js에서 스크롤 위치 유지하기 (Scroll Restoration)

시소·2023년 11월 23일
8
post-thumbnail

🚪 들어가며

사람들은 웹 사이트를 이용하다가, 상세페이지 같이 다른 페이지로 이동 후에 브라우저의 뒤로 가기 버튼을 눌러 기존 페이지로 되돌아왔을 때 어떤 동작을 기대할까?
예를 들어 무한 스크롤이 적용된 쇼핑몰 페이지가 있는데, 스크롤을 주욱 아래로 내려서 상품 상세 페이지로 잠깐 이동했다가 뒤로 가기 버튼을 눌렀는데 스크롤이 최상단으로 가 있고 이전에 불러온 상품들이 초기화 된다면, 이전에 봤던 곳까지 도달하려면 또 다시 스크롤을 한참 내려야 할 것이며 사용자는 피로감을 느낄 것이다.

이러한 이유로 스크롤이 이전 위치에 유지되는 동작은 일반적인 요구사항이고, 많은 사용자들은 그러한 동작을 기대할 수 있다. 스크롤이 유지된다면 사용자들은 전에 보고 있던 부분으로 복귀하기 위해 많은 노력을 기울이지 않아도 원활하게 내용을 탐색할 수 있게 되고 이는 사용자 경험 향상으로 이어질 수 있게 된다.

따라서 이번 포스팅에서는 스크롤 위치를 유지하기 위한 방법에 대해(특히, Next.js 환경에서) 알아 보고자 한다.

📝 사전 지식

History APIhistory 객체

본격적으로 코드에 대해 먼저 이야기 하기 전에, JavaScript에서 스크롤을 다룰 수 있는 방법이 있나 하고 찾아보았다. 그랬더니 해당 내장 메서드 및 객체가 소개되었다.

History API 자체가 스크롤을 관리하기 위한 것이라고는 하기 어렵고, 사실은 웹 페이지 열람 이력을 관리하도록 제공되는 기능이다.
해당 API는 브라우저의 세션 히스토리에 대한 정보를 가지는 JavaScript의 window.history 전역 객체로의 접근을 제공하는데, history 객체를 조작할 수 있는 여러 메서드 및 프로퍼티를 사용할 수 있다.
그 중에서 브라우저 스크롤과 관련된 내용은 바로 다음과 같다.

  • scrollRestoration 프로퍼티: 스크롤 복원과 관련된 부분을 담당하는 프로퍼티로, 값으로는 "auto"(기본값, 웹 페이지 이동 시 스크롤 위치 자동 복원) 또는 "manual"(개발자가 모든 스크롤 변경 사항 수동으로 담당) 중 하나를 가진다.

값이 "auto" 로 설정되어 있으면 개발자가 해당 부분과 관련해 신경 쓸 필요가 없겠지만, 해당 설정을 사용할 수 없는 상황이라던가 스크롤에 대해 직접 제어를 하고자 한다면, 예를 들어 다음과 같이 구현할 수 있겠다.

// 현재 설정 값을 확인하고, Scroll resotration 값을 "manual"로 설정
if ("scrollRestoration" in history && history.scrollRestoration !== "manual") {
  history.scrollRestoration = "manual";
}

// 페이지 로드 시, Session storage에 저장된 스크롤 위치 불러 오기
window.onload = () => {
  const storedPos = sessionStorage.getItem("scrollPos");

  if (storedPos) {
    window.scrollTo(0, parseInt(storedPos));
    sessionStorage.removeItem("scrollPos");
  }
};

// 페이지 이동 시, 스크롤 위치를 Session storage에 저장
window.onbeforeunload = () => {
  sessionStorage.setItem("scrollPos", window.scrollY.toString());
};

위 코드는 설명을 위해 Vanilla JS로 작성한 예시 코드이며, 환경에 따라 적용 방법은 달라질 수 있다. 이 다음 대목에서부터는 Next.js 프로젝트에서 이러한 내용을 달성하려면 어떻게 할 수 있는지에 관해 담아 보았다.


🖱️ Next.js 에서 스크롤 유지

시나리오

우선, 웹 페이지에서 흔하게 맞닥뜨릴 수 있는 상황으로 가정해 보았다. 메인 페이지의 컨텐츠는 무한 스크롤로 구현 되어 있으며, 게시글 목록은 10개씩 페이징되며 다음 페이지가 존재할 시 하단의 버튼을 클릭하여 항목을 추가로 불러올 수 있다. 이와 같은 상황에서,

  1. 스크롤을 내려 3페이지에 해당하는 항목 로드
  2. 30번째 항목에 해당하는 게시글 클릭 (상세페이지로 이동)
  3. 그 후 브라우저의 뒤로가기 버튼 클릭
  4. 현재 보고 있는 화면에는 이전에 불러온 30개 항목이 남아있어야 하고, 스크롤도 이전에 보고 있던 곳에 위치시키고자 함

위와 같은 동작이 가능하도록 구현할 수 있어야 한다.

scrollRestoration 옵션을 사용하면 해결될까?

Next.js에서 제공하는 기능 중, 스크롤 위치 유지와 관련한 내용으로 다음과 같은 옵션이 있었다.

// next.config.js
module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

바로 새 프로젝트를 만들어 테스트를 해 보았는데 해당 옵션 true/false 값에 따라 history.scrollResoration = "auto", history.scrollRestoration = "manual" 처럼 동작하는 것을 확인해 보았다. 잘 되는 것 같다.

하지만 "experimental" 기능 이라는 것.. 즉 실험적 기능이라는 점이 마음에 걸린다. 어느날 갑자기 사라질 수도 있지 않을까 혹은 추후 원하는 대로 동작하지 않을 수도 있지 않을까 하고 걱정이 된다. 또한 version 14 이전에는 공식 문서에서 해당 내용을 찾아볼 수 있었던 것 같은데, 최신 버전의 Document에서는 검색조차 되지 않았다.

그리하여 개발자가 직접 스크롤이 처리되는 방식을 제어해야 하는 상황이 발생할 수도 있을 것 같다고 생각이 들어, 스크롤 로직을 수동으로 처리할 수 있는 방법에 대해 알아보았다.

scrollRestoration 기본 값

next.config.js 에서 experimental.scrollRestoration 옵션을 지정하지 않고, 컴포넌트 안에서 history.scrollRestoration의 default 값이 어떤 값으로 찍히는 지 확인해 보았다.

useEffect(() => {
  if (!"scrollRestoration" in history) return;
  console.log(history.scrollRestoration);
}, []);

그랬더니, "auto" 가 기본 값으로 설정되어 있었다. 하지만 브라우저 상에서는 실제로 스크롤 복원이 예상한 대로 동작하지 않았고 페이지 이동 시 항상 스크롤이 맨 위에 위치하게 되었다.

useEffect 내부에서 history.scrollRestoration = "auto"로 값을 다시 변경해보기도 하고 아래와 같이 _document.js 파일에서도 해당 값 변경을 해 보았는데, 콘솔에는 "auto" 라고 찍히지만 역시나 기대한 대로 동작하지는 않았다.

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script
          dangerouslySetInnerHTML={{
            __html: 'history.scrollRestoration = "auto"',
          }}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

그리하여 Next.js의 내장 Router 기능 및 SessionStorage를 활용하여 스크롤 동작을 제한하도록 하여 원하는 동작을 구현하게 되었다.

드디어 실제 코드 (커스텀 훅 구현)

시나리오 대로 만들기 위해 Next.js 프로젝트에 다음 내용을 이용해 토대를 만들었다.

  • Mock Data: DummyJson
  • Data Fetching: axiosSWR (무한 스크롤 기능 위해 useSWRInfinite 사용해 보았다)
  • UI Library: MUI

가장 중요한 부분인 스크롤을 제어하는 부분은 아래와 같이 커스텀 훅을 만들어 _app.js에 적용시켰다.

// src/hooks/useScrollRestoration.js

function saveScrollPos() {
  // 현재 스크롤 위치를 세션 스토리지에 저장
  sessionStorage.setItem("scrollPos", window.scrollY);
}

function restoreScrollPos() {
  // 복원시킬 스크롤 위치가 존재하면 
  // 브라우저 스크롤 위치를 해당 값으로 이동시킨 후, 세션 스토리지에서 이전 위치 정보 제거
  const scrollPos = sessionStorage.getItem("scrollPos");
  if (scrollPos) {
    window.scrollTo(0, scrollPos);
    sessionStorage.removeItem("scrollPos");
  }
}

export default function useScrollResotration(router) {
  // 스크롤 복원 여부 확인을 위해 사용되는 flag 값
  const shouldScrollRestore = useRef(false);

  useEffect(() => {
    if ("scrollRestoration" in window.history) {
      shouldScrollRestore.current = false;
      // 스크롤 복원 기본 동작을 수동으로 제어
      window.history.scrollRestoration = "manual";

      // 페이지 이동 시작 시 스크롤 위치 저장
      const onRouteChangeStart = () => {
        if (!shouldScrollRestore.current) {
          saveScrollPos();
        }
      };

      // (브라우저 뒤로 가기 동작으로 인한) 페이지 이동 완료 시 
      // 스크롤을 이전 위치로 복원
      const onRouteChangeComplete = () => {
        if (shouldScrollRestore.current) {
          shouldScrollRestore.current = false;
          restoreScrollPos();
        }
      };

      router.events.on("routeChangeStart", onRouteChangeStart);
      router.events.on("routeChangeComplete", onRouteChangeComplete);
      router.beforePopState(() => {
        shouldScrollRestore.current = true;
        return true;
      });

      return () => {
        router.events.off("routeChangeStart", onRouteChangeStart);
        router.events.off("routeChangeComplete", onRouteChangeComplete);
        router.beforePopState(() => true);
      };
    }
  }, [router]);
}
// src/pages/_app.js

export default function App({ Component, pageProps, router }) {
  useScrollResotration(router);

  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

전체 코드

아래 링크에서 확인할 수 있다.
https://github.com/mnngfl/next-js-scroll-restoration-with-infinite-scroll


👀 참고

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

4개의 댓글

comment-user-thumbnail
2024년 3월 29일

잘봤습니다.!

1개의 답글
comment-user-thumbnail
2024년 4월 8일

딱 찾던 내용이네요 감사합니다!

1개의 답글