통합검색

Javascript

[GSAP] fullpage 대체 효과 구현

  • 2025.11.05 08:32:38


바닐라스크립트 + GSAP 환경에서 jquery 의 fullpage 효과를 대체하는 효과.
GSAP 의 ScrollToPlugin 플러그인을 활용해 구현 가능하다.
코드가 제공하는 기능은 아래와 같다.

- 마우스 스크롤시 섹션 자동 이동
- 모바일에서는 fullpage 효과 해제 (리사이징시에도 적용)
- 섹션 이동시 주소표시줄에 해시(#) 적용
- 해시로 접속한 경우 해당 섹션 자동으로 이동
- navigation 적용



[!]HTML 코드[/!] 
<!-- 컨텐츠 섹션 -->
<div id="content">
    <div class="panel">첫번째 섹션</div>
    <div class="panel">두번째 섹션</div>
    <div class="panel">세번째 섹션</div>
</div>

<!-- navigation -->
<div id="pagination">
    <ul>
        <li><a href="#"><p>첫번째 섹션으로</p></a></li>
        <li><a href="#"><p>두번째 섹션으로</p></a></li>
        <li><a href="#"><p>세번째 섹션으로</p></a></li>
    </ul>
</div>

[!]Javascript(GSAP) 코드[/!] 
////////////////////
// fullpage 대체 효과
////////////////////
gsap.registerPlugin(ScrollToPlugin);

const panels   = document.querySelectorAll('.panel');
const navLinks = document.querySelectorAll('#pagination a');

// 1200px 이상에서만 작동되도록
let isDesktop = window.matchMedia('(min-width: 1200px)').matches;
const mql = window.matchMedia('(min-width: 1200px)');

let isScrolling = false;
let currentIndex = 0;

// 해시로 진입했을 때 처리 (#1, #2 ...)
initFromHash();

// 뷰포트 변경 시 데스크톱 여부 갱신
mql.addEventListener('change', (e) => {
    isDesktop = e.matches;
});

// 스크롤시 navigation 메뉴 active 처리 및 location.hash 처리
function setActive(idx) {
    navLinks.forEach(a => a.classList.remove('on'));
    if (navLinks[idx]) navLinks[idx].classList.add('on');
    
    const hash = '#' + (idx + 1);
    if (location.hash !== hash) {
        // replaceState 쌓이지 않도록
        history.replaceState(null, '', hash);
    }
}

// 해시로 들어왔을 때 초기 위치 맞추기
function initFromHash() {
    const h = location.hash.replace('#', '');
    const num = parseInt(h, 10);
    if (!isNaN(num) && num >= 1 && num <= panels.length) {
        currentIndex = num - 1;
        gsap.set(window, { scrollTo: panels[currentIndex] });
        setActive(currentIndex);
    } else {
        setActive(0);
    }
}

// pc에서만 휠로 섹션 이동
window.addEventListener('wheel', (e) => {
    if (!isDesktop) return;
    if (isScrolling) return;
    e.preventDefault();

    if (e.deltaY > 0 && currentIndex < panels.length - 1) {
        currentIndex++;
    } else if (e.deltaY < 0 && currentIndex > 0) {
        currentIndex--;
    } else {
        return;
    }

    scrollToPanel(currentIndex);
}, { passive: false });

// navigation 메뉴 클릭
navLinks.forEach((a, i) => {
    a.addEventListener('click', (e) => {
        e.preventDefault();
        if (isScrolling || i === currentIndex) return;
        currentIndex = i;
        scrollToPanel(currentIndex);
    });
});

// 스크롤 이동 효과 (gsap)
function scrollToPanel(index) {
    isScrolling = true;
    if (isDesktop) disableScroll();

    gsap.to(window, {
        duration: 1.1,
        scrollTo: panels[index],
        ease: "power3.inOut",
        onComplete() {
            setActive(index);
            setTimeout(() => {
                isScrolling = false;
                enableScroll();
            }, 200);
        }
    });
}

// 이동하는 도중에는 스크롤 잠금
function disableScroll() {
    window.addEventListener('wheel', prevent, { passive: false });
    window.addEventListener('touchmove', prevent, { passive: false });
    window.addEventListener('keydown', preventForKeys, { passive: false });
}
function enableScroll() {
    window.removeEventListener('wheel', prevent, { passive: false });
    window.removeEventListener('touchmove', prevent, { passive: false });
    window.removeEventListener('keydown', preventForKeys, { passive: false });
}
function prevent(e) { e.preventDefault(); }
function preventForKeys(e) {
    const keys = [32,33,34,35,36,37,38,39,40];
    if (keys.includes(e.keyCode)) e.preventDefault();
}