부드러운 스크롤 구현을 위한 locomotive-scroll
데스크탑에서도 모바일에서 스크롤 하듯 부드러운 스크롤을 구현하려면 어떻게 해야 할까?
locomotive-scroll과 같은 라이브러리를 사용하면 간단하게 부드러운 스크롤 구현이 가능하다.
부드러운 스크롤이 적용된 페이지 예시로는 지마켓 채용 페이지가 있다.
보통은 원 페이지 단위의 정적 페이지에서 주로 사용하는데, 멀티 페이지 환경인 Next.js에서도 사용이 가능하다.
어떻게 사용해야 하는지 알아보자.
Next.js에서 locomotive-scroll을 사용하려면?
일반적으로 라이브러리를 사용할 때는 파일 상단에서 import 명령어를 통해 모듈을 임포트한다.
locomotive-scroll의 경우에는 즉시 실행 함수 내에 dom에 접근하는 코드가 있기 때문에 일반적인 방법으로 임포트시 아래와 같이 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;
});
}, []);
참고 링크