https://codepen.io/phillip-gimmi/pen/XWOdEMW
React Smooth Section Navigator
An interactive React application that provides seamless navigation between sections of content using the wheel or touch gestures. It employs GSAP's (Gr...
codepen.io
코드펜에서 발견한 섹션 전환 애니메이션을 프로젝트에 적용시켰다.
gsap은 처음 사용하기 때문에 코드를 하나씩 살펴보고 프로젝트에 적용시키는 과정을 포스팅하고자 한다.
gsap을 사용하려면 먼저 gsap을 설치해야 한다.
npm install gsap
import React, { useEffect } from 'react';
import gsap from 'gsap';
import './MainPage.css';
function MainPage() {
useEffect(() => {
}, []);
return (
<>
</>
);
}
export default MainPage;
리액트 컴포넌트가 렌더링 된 직후에 애니메이션 효과가 실행되도록 useEffect() 안에 애니메이션을 설정하는 코드를 작성할 것이다. 그전에 먼저 화면에서 애니메이션이 적용될 섹션을 생성한다.
<>
<section className="section">
<div className="wrapperOuter">
<div className="wrapperInner">
<div className="background" style={{ backgroundColor: 'lightgray' }} />
</div>
</div>
</section>
<section className="section">
<div className="wrapperOuter">
<div className="wrapperInner">
<div className="background" style={{ backgroundColor: 'gray' }} />
</div>
</div>
</section>
<section className="section">
<div className="wrapperOuter">
<div className="wrapperInner">
<div className="background" style={{ backgroundColor: 'darkgray' }} />
</div>
</div>
</section>
</>
useEffect 안에서 섹션을 가져오고 애니메이션 함수를 생성한다.
useEffect(() => {
let sections = document.querySelectorAll('section'),
images = document.querySelectorAll('.background'),
outerWrappers = document.querySelectorAll('.wrapperOuter'),
innerWrappers = document.querySelectorAll('.wrapperInner');
let currentIndex = -1;
let wrap = (index, max) => (index + max) % max;
let animating;
const gotoSection = (index, direction) => {
index = wrap(index, sections.length);
}
}, []);
currentIndex는 현재 활성화된 섹션의 인덱스이다. 초기에 아무것도 없는 화면에서 첫 번째 섹션이 활성화되도록 하기 위해 -1로 초기화한다.
wrap 함수는 주어진 인덱스를 순환하는 함수이다. 인덱스가 음수이거나 max값을 초과해도 순환 형식으로 동작하기 때문에 유효한 인덱스를 반환한다.
animation은 애니메이션이 동작 중인지를 나타내기 위한 변수이며 gotoSection 함수가 호출될 때 true로 설정한다.
direction은 애니메이션의 방향이다.
currentIndex가 -1이기 때문에 처음엔 아무것도 활성화되지 않은 상태이다.
렌더링이 되고 useEffect 훅이 실행되면 gotoSection(0, 1)이 호출된다.
index가 0으로 설정됨에 따라 currentIndex는 0으로 업데이트되고 따라서 첫번째 섹션이 활성화된다.
이제 gsap의 타임라인을 생성한다. 타임라인은 여러 개의 애니메이션을 쉽게 조작하고 동기화할 수 있는 일종의 컨트롤러다.
const gotoSection = (index, direction) => {
index = wrap(index, sections.length);
animating = true;
let timeline = gsap.timeline({
defaults: { duration: 1.25, ease: "power1.inOut" },
onComplete: () => (animating = false)
}
defaults는 타임라인 안의 모든 애니메이션에 공통으로 적용될 기본 옵션을 정의한다.
power1.inOut은 애니메이션이 느리게 시작해서 빠르게 진행되다 다시 느리게 끝나는 효과를 만든다.
onComplete는 애니메이션이 완료되었을 때 실행되는 함수로, animating이 false로 설정하여 애니메이션이 끝났음을 표시하도록 한다.
동작을 더 빠르게 혹은 더 느리게 하고 싶다면 duration 값을 조정하면 된다.
이제 애니메이션 동작 로직을 설정한다.
onComplete: () => (animating = false)
})
if (currentIndex >= 0) {
gsap.set(sections[currentIndex], { zIndex: 0 });
timeline.to(images[currentIndex], { yPercent: -15 * direction })
.set(sections[currentIndex], { autoAlpha: 0 });
}
currentIndex = index;
현재 활성화된 섹션을 처리하는 부분이다. 새로운 섹션을 앞으로 올리기 위해 zIndex를 0으로 설정하여 뒤로 보낸다.
yPercent를 사용하여 수직으로 15% 이동시키는데, direction을 곱하여 스크롤 방향에 따라 위 또는 아래로 이동하도록 한다.
autoAlpha는 opacity와 visibility를 동시에 제어하는 gsap 속성이다. 0으로 설정하여 화면에서 사라진 것처럼 만든다.
처음 시작할 땐 currentIndex가 -1이기 때문에 위 로직은 실행되지 않는다.
.set(sections[currentIndex], { autoAlpha: 0 });
}
gsap.set(sections[index], { autoAlpha: 1, zIndex: 1 });
currentIndex = index;
새로운 섹션을 앞에 표시한다.
gsap.set(sections[index], { autoAlpha: 1, zIndex: 1 });
timeline.fromTo(
[outerWrappers[index], innerWrappers[index]],
{ yPercent: (i) => (i ? -100 * direction : 100 * direction) },
{ yPercent: 0 },
0
)
섹션 내의 outerWrappers와 innerWrappers를 움직이는 애니메이션이다. i 값에 따라 두 wrapper를 서로 반대 방향으로 이동시키고 yPercent: 0으로 최종적으로 중앙에 위치시킨다.
0
).fromTo(images[index], { yPercent: 15 * direction }, { yPercent: 0 }, 0)
섹션 이미지를 위 또는 아래 방향으로 이동시킨다.
이렇게 섹션 인덱스와 스크롤 방향을 매개변수로 받아 동작하는 gotoSection 함수가 완성되었다.
const gotoSection = (index, direction) => {
index = wrap(index, sections.length);
animating = true;
let timeline = gsap.timeline({
defaults: { duration: 1.25, ease: "power1.inOut" },
onComplete: () => (animating = false)
})
if (currentIndex >= 0) {
gsap.set(sections[currentIndex], { zIndex: 0 });
timeline.to(images[currentIndex], { yPercent: -15 * direction })
.set(sections[currentIndex], { autoAlpha: 0 });
}
gsap.set(sections[index], { autoAlpha: 1, zIndex: 1 });
timeline.fromTo(
[outerWrappers[index], innerWrappers[index]],
{ yPercent: (i) => (i ? -100 * direction : 100 * direction) },
{ yPercent: 0 },
0
).fromTo(images[index], { yPercent: 15 * direction }, { yPercent: 0 }, 0)
currentIndex = index;
}
저장하고 실행을 시키면 빈 화면에 첫번째 섹션이 올라오는 애니메이션 효과를 볼 수 있다.
이제 스크롤 이벤트 핸들러를 작성하여 스크롤에 따른 섹션 전환 효과가 나타날 수 있도록 한다.
const handleWheel = (event) => {
if (!animating) {
const direction = event.deltaY > 0 ? 1 : -1;
gotoSection(currentIndex + direction, direction);
}
}
gotoSection(0, 1);
animating이 false일 때만 함수의 내용을 실행한다. 애니메이션이 진행 중일 때 중복으로 실행되는 상황을 막기 위해서이다.
event.deltaY는 마우스 휠이 스크롤된 방향이다. deltaY 값이 양수면 아래로 스크롤, 음수면 위로 스크롤했음을 의미한다.
이벤트 핸들러를 윈도우 객체에 추가한다.
const addEventListeners = () => {
window.addEventListener('wheel', handleWheel);
}
gotoSection(0, 1);
addEventListeners();
사용자가 마우스 휠을 움직일 때 발생하는 이벤트를 수신한다.
컴포넌트가 언마운트될 때 불필요한 이벤트 처리기를 제거할 수 있도록 이벤트 리스너 제거 함수도 작성한다.
const addEventListeners = () => {
window.addEventListener('wheel', handleWheel);
}
const removeEventListeners = () => {
window.removeEventListener('wheel', handleWheel);
}
gotoSection(0, 1);
addEventListeners();
return () => removeEventListeners();
스크롤에 따른 섹션 전환 효과가 잘 적용되었다.
이제 모바일에서도 같은 효과가 나타나도록 터치 이벤트도 추가해준다.
const handleTouch = (event) => event.changedTouches[0].screenY;
터치 이벤트의 changedTouches 속성을 이용하여 사용자가 화면을 터치한 좌표의 y 좌표를 가져온다.
const addEventListeners = () => {
window.addEventListener('wheel', handleWheel);
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
touchStartY = handleTouch(e);
});
window.addEventListener('touchend', (e) => {
const touchEndY = handleTouch(e);
if (!animating) {
const direction = touchEndY < touchStartY ? 1 : -1;
gotoSection(currentIndex + direction, direction);
}
});
};
const removeEventListeners = () => {
window.removeEventListener('wheel', handleWheel);
window.removeEventListener('touch', handleTouch);
}
사용자가 화면을 터치한 순간의 y좌표 값을 touchStartY에 저장하고, 화면에서 손을 떼는 순간의 y좌표 값을 touchEndY에 저장한다. 두 값을 비교하여 스와이프한 방향에 따라 direction 값을 결정한다.
이제 모바일에서도 같은 전환 효과를 볼 수 있다.