Frontend/React

Next.js에서 locomotive-scroll 사용하기

둉이 2024. 6. 3. 14:00

부드러운 스크롤 구현을 위한 locomotive-scroll

데스크탑에서도 모바일에서 스크롤 하듯 부드러운 스크롤을 구현하려면 어떻게 해야 할까?

 

locomotive-scroll과 같은 라이브러리를 사용하면 간단하게 부드러운 스크롤 구현이 가능하다.

 

부드러운 스크롤이 적용된 페이지 예시로는 지마켓 채용 페이지가 있다.

 

G마켓 인재영입 - 지마켓 채용 사이트

Gmarket Careers, 지마켓 채용 사이트, 세상에 가치를 연결하고 당신이 가져올 변화를 기다립니다.

careers.gmarket.com

 

 

보통은 원 페이지 단위의 정적 페이지에서 주로 사용하는데, 멀티 페이지 환경인 Next.js에서도 사용이 가능하다.

 

어떻게 사용해야 하는지 알아보자.

 

 

Next.js에서 locomotive-scroll을 사용하려면?

일반적으로 라이브러리를 사용할 때는 파일 상단에서 import 명령어를 통해 모듈을 임포트한다.

 

locomotive-scroll의 경우에는 즉시 실행 함수 내에 dom에 접근하는 코드가 있기 때문에 일반적인 방법으로 임포트시 아래와 같이 document를 찾을 수 없다는 에러가 뜬다.

next.js에서 일반적인 방법으로 locomotive-scroll 임포트시 뜨는 에러

 

locomotive-scroll에서 문제가 되는 document 접근 부분

 

 

따라서 next.js에서 locomotive-scroll을 사용하기 위해서는 useEffect 내에서 import() 함수를 사용하여 모듈을 동적으로 임포트해야 한다.

useEffect(() => {
    if (!containerRef.current) return;

    // 동적으로 locomotive-scroll import
    const loadLocomotiveScroll = async () => {
      const { default: LocomotiveScroll } = await import('locomotive-scroll', { with: {} });

      const newScroll = new LocomotiveScroll({
        el: containerRef.current as HTMLElement,
        smooth: true,
        resetNativeScroll: true,
        smartphone: { smooth: true },
      });
      setScroll(newScroll);
    };
    loadLocomotiveScroll();

    if (!scroll) return;

    const handleUpdateScroll = () => {
      try {
        scroll?.update();
      } catch {}
    };

    // 페이지 컨텐츠 로드, 뷰포트 리사이즈 시마다 스크롤 변경사항 업데이트
    window.addEventListener('DOMContentLoaded', handleUpdateScroll);
    window.addEventListener('resize', handleUpdateScroll);

    return () => {
      window.removeEventListener('DOMContentLoaded', handleUpdateScroll);
      window.removeEventListener('resize', handleUpdateScroll);
      scroll?.destroy();
      setScroll(null);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

 

 

useEffect 훅은 컴포넌트가 마운트된 후 호출되므로, document 객체에 접근할 수 있다.

 

 

이슈 1 ) csr 페이지 이동시 스크롤 영역 높이가 달라지는 이슈

next/router 혹은 next/link를 사용하여 페이지 이동시, 클라이언트 사이드에서 페이지 이동이 일어난다.

 

이러한 경우에는 locomotive-scroll가 스크롤 영역의 높이 변경을 감지하지 못해 스크롤 영역이 이전 페이지 기준으로 잡히는 이슈가 있다.

 

우리는 위에서 resize 이벤트가 발생할 때마다 scroll 변경사항을 업데이트 하도록 코드를 추가했다.

window.addEventListener('resize', handleUpdateScroll);

return () => {
  window.removeEventListener('resize', handleUpdateScroll);
};

 

 

이를 활용하여 페이지 컴포넌트가 달라질 때마다 resize event를 수동으로 호출하여 대응할 수 있다.

const App = ({ Component, pageProps }: AppProps) => {
  // 컴포넌트 변경 시마다 resize 이벤트 호출
  useEffect(() => {
    window.dispatchEvent(new Event('resize'));
  }, [Component]);

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

 

 

하지만 이 방법은 이미지 혹은 동영상 지연 로딩으로 layout shift가 발생하는 경우까지는 막을 수가 없다는 단점이 있다.

 

모든 케이스의 스크롤 높이 변경에 대응하려면 ResizeObserver를 사용하여 스크롤 영역을 감시하면 된다.

useEffect(() => {
    if (!containerRef.current) return;

    const observer = new ResizeObserver(() => {
      window.dispatchEvent(new Event('resize'));
    });
    observer.observe(containerRef.current);

    return () => {
      observer.disconnect();
    };
}, []);

 

 

 

이슈 2) 스크롤 복원 이슈

새로고침 혹은 뒤로가기로 페이지 진입시, 기본적으로 nextjs router가 이전 스크롤 위치를 저장하여 복원을 수행한다.

 

locomotive-scroll 사용 도중 스크롤이 복원되면 transform 값과 스크롤 위치가 꼬이면서 복원된 위치 상단으로는 이동할 수 없는 이슈가 발생한다.

 

따라서 다음과 같이 공통 레이아웃 혹은 페이지 컴포넌트 상단에 코드를 추가하여 자동 스크롤 복원을 사용하지 않도록 처리해야 한다.

useEffect(() => {
    router.beforePopState((state) => {
      state.options.scroll = false;
      return true;
    });
}, []);

 

 

 

참고 링크

 

How to correctly use Locomotive Scroll with Next.js routing?

I'm using locomotive-scroll with Next.js and all working fine. But after route to a different page, my scroll won't destroy and 2 scrolls overlap each other. How to correctly reinit locomotive-scro...

stackoverflow.com

 

 

Can't scroll down after page changed · Issue #332 · locomotivemtl/locomotive-scroll

Hello 👋 issue I have installed Locomotive scroll and React Transition Group in my NextJS app. but when I click on the link or open a page I can't scroll anymore and it will stick at the top of the ...

github.com