프로젝트 중 FullPage 스크롤 구현이 필요하여 라이브러리 없이 제작해보았다.
구현 포인트
화면구조
body 태그에 overflow-y: hidden; 속성을 주고 화면 높이 만큼을 가지고 있는 section 태그에 overflow-y: auto; (outer 클래스 명) 속성을 주어 사용자가 페이지 전체를 스크롤 하지 못하게 방지하며 outer 섹션 내부에서만 스크롤이 가능하게 한다.
outer 섹션안에 fullPage를 만들고 싶은 화면 높이 만큼을 가지고 있는 section 들과 footer를 넣어준다.
html
<section ref={outerDivRef} className="outer"> <section className="bg-red-100 inner"></section> <section className="bg-red-200 inner"></section> <section className="bg-red-300 inner"></section> <section className="bg-red-400 inner"></section> <FooterTest /> </section>
css
body { overflow-y: hidden; } .outer { height: 100vh; overflow-y: auto; } .inner { position: relative; height: 100vh; }
스크롤 동작
scrollTo 함수 설명
스크롤 할 목표 위치(top)로 주어진 duration 시간(애니메이션이 완료되는 데 걸리는 시간(밀리초)) 동안 부드럽게 스크롤 하는 애니메이션을 수행한다.
animateScroll 함수 설명
const outerDivRef = useRef(null) const linear = (t) => t const scrollTo = (top, duration) => { const start = Math.ceil(outerDivRef.current.scrollTop) // 현재 스크롤 위치 const change = top - start // 목표 위치까지의 거리 const startTime = performance.now() // 애니메이션 시작 시간 const animateScroll = (currentTime) => { const timeElapsed = currentTime - startTime // 경과 시간 const progress = Math.min(timeElapsed / duration, 1) // 진행 상황 (0에서 1 사이의 값) outerDivRef.current.scrollTop = start + change * linear(progress) // 스크롤 위치 업데이트 if (timeElapsed < duration) { // 애니메이션이 아직 끝나지 않음 requestAnimationFrame(animateScroll) // 다음 프레임 요청 } } requestAnimationFrame(animateScroll) // 첫 번째 애니메이션 프레임 요청 }
wheelHandler 함수 설명
사용자의 마우스 휠 스크롤 이벤트를 처리하여 스크롤 동작을 제어해 페이지의 특정 구역(예: 페이지나 푸터)으로 스크롤을 부드럽게 이동시키는 역할을 한다.
아래로 스크롤
if (deltaY > 0) { //아래로 스크롤하는 경우. nextPage = Math.ceil(scrollTop / pageHeight) + 1 if (scrollTop + deltaY >= maxScrollTop - footerHeight) { scrollTo(maxScrollTop, scrollDuration) return } }
위로 스크롤
else { nextPage = Math.ceil(scrollTop / pageHeight) - 1 if (nextPage < 0) { nextPage = 0 } if (scrollTop > maxScrollTop - footerHeight) { scrollTo(maxScrollTop - footerHeight, scrollDuration) return } }
일반적인 페이지 스크롤
nextPage >= 0 && nextPage <= maxPage : 다음 페이지가 유효한 범위(0에서 최대 페이지) 내에 있는 경우 다음 페이지로 스크롤한다.
if (nextPage >= 0 && nextPage <= maxPage) { scrollTo(nextPage * pageHeight, scrollDuration) }
wheelHandler 함수 전체코드
const wheelHandler = (e) => { // 수직 스크롤 동작 e.preventDefault() const { deltaY } = e //deltaY 양수이면 아래로 스크롤 / 음수이면 위로 스크롤 const { scrollTop, clientHeight, scrollHeight } = outerDivRef.current const pageHeight = clientHeight // 페이지 높이 const footerHeight = 382 // footer의 높이 const maxScrollTop = scrollHeight - clientHeight // 최대 스크롤 가능한 위치 const maxPage = 3 // 총 페이지 수 - 1 const scrollDuration = 600 // 스크롤 속도 조절 (밀리초 단위) let nextPage if (deltaY > 0) { // 아래로 스크롤 nextPage = Math.ceil(scrollTop / pageHeight) + 1 // 아래로 스크롤 & 마지막 페이지 경우 if (scrollTop + deltaY >= maxScrollTop - footerHeight) { scrollTo(maxScrollTop, scrollDuration) // 정확히 footerHeight만큼 return } } else { // 위로 스크롤 nextPage = Math.ceil(scrollTop / pageHeight) - 1 if (nextPage < 0) { nextPage = 0 // 페이지가 0보다 작으면 0으로 설정 } // 위로 스크롤 & 마지막 페이지 경우 if (scrollTop > maxScrollTop - footerHeight) { scrollTo(maxScrollTop - footerHeight, scrollDuration) // 정확히 footerHeight만큼 return } } if (nextPage >= 0 && nextPage <= maxPage) { // 일반적인 페이지 스크롤인 경우 scrollTo(nextPage * pageHeight, scrollDuration) } }
전체코드
'use client' import FooterTest from '@/component/footerTest' import { useEffect, useRef } from 'react' export default function Test() { const outerDivRef = useRef(null) const linear = (t) => t // linear 효과 const scrollTo = (top, duration) => { const start = Math.ceil(outerDivRef.current.scrollTop) const change = top - start const startTime = performance.now() const animateScroll = (currentTime) => { const timeElapsed = currentTime - startTime const progress = Math.min(timeElapsed / duration, 1) // 0에서 1 사이의 값 outerDivRef.current.scrollTop = start + change * linear(progress) if (timeElapsed < duration) { requestAnimationFrame(animateScroll) } } requestAnimationFrame(animateScroll) } const wheelHandler = (e) => { // 수직 스크롤 동작 e.preventDefault() const { deltaY } = e //deltaY 양수이면 아래로 스크롤 / 음수이면 위로 스크롤 const { scrollTop, clientHeight, scrollHeight } = outerDivRef.current const pageHeight = clientHeight // 페이지 높이 const footerHeight = 382 // footer의 높이 const maxScrollTop = scrollHeight - clientHeight // 최대 스크롤 가능한 위치 const maxPage = 3 // 총 페이지 수 - 1 const scrollDuration = 600 // 스크롤 속도 조절 (밀리초 단위) let nextPage if (deltaY > 0) { // 아래로 스크롤 nextPage = Math.ceil(scrollTop / pageHeight) + 1 // 아래로 스크롤 & 마지막 페이지 경우 if (scrollTop + deltaY >= maxScrollTop - footerHeight) { scrollTo(maxScrollTop, scrollDuration) // 정확히 footerHeight만큼 return } } else { // 위로 스크롤 nextPage = Math.ceil(scrollTop / pageHeight) - 1 if (nextPage < 0) { nextPage = 0 // 페이지가 0보다 작으면 0으로 설정 } // 위로 스크롤 & 마지막 페이지 경우 if (scrollTop > maxScrollTop - footerHeight) { scrollTo(maxScrollTop - footerHeight, scrollDuration) // 정확히 footerHeight만큼 return } } if (nextPage >= 0 && nextPage <= maxPage) { // 일반적인 페이지 스크롤인 경우 scrollTo(nextPage * pageHeight, scrollDuration) } } useEffect(() => { const outerDivRefCurrent = outerDivRef.current outerDivRefCurrent.addEventListener('wheel', wheelHandler) return () => { outerDivRefCurrent.removeEventListener('wheel', wheelHandler) } }, []) return ( <> <section ref={outerDivRef} className="outer"> <section className="bg-red-100 inner"></section> <section className="bg-red-200 inner"></section> <section className="bg-red-300 inner"></section> <section className="bg-red-400 inner"></section> <FooterTest /> </section> </> ) }
결과