리액트 기반 세로로 배치된 5개의 이미지들을 사용자가 스크롤함에 따라서 나타나도록 애니메이션을 구현해봅시다~!
먼저 리액트에서 애니메이션을 구현하는 방법은 다양하게 존재하지만, 외부 라이브러리를 사용하지 않는 방법과 기본 API만 사용하는 방법을 포함해 총 3가지로 구현해보았습니다.
Result
깃허브 링크: https://github.com/abyss-s/react-scroll-animation
배포 링크: https://react-scroll-animation-orcin.vercel.app/
1. IntersectionObserver
IntersectionObserver는 순수 JavaScript API로, 라이브러리에 의존하지 않고도 스크롤 애니메이션을 구현할 수 있습니다. Intersection Observer는 내가 관찰하고 싶은 특정 타겟 엘리먼트가 자기 자신의 조상 엘리먼트들 중 정해진 누군가, 혹은 최상위 문서 등 관찰하고자 하는 요소가 의 뷰포트(화면)에 들어왔는지 비동기적으로 감지합니다.
이 개념은 지연 로딩에 의해 정의됩니다. 지연로딩(lazy loading)이란? 주로 불러올 양이 많거나 스크롤에 따라 데이터를 다르게 불러오는 경우, 불러올 컨텐츠만 레이지하게 로딩해야 합니다. 이에 따라 빠른 속도로 사용자 경험을 향상시키는 것은 물론 광고 수익산정을 위해 가시성을 보고하거나, 사용자가 해당 결과를 보는지 여부에 따라 애니메이션 프로세스 적용 여부를 결정할 수 있기 때문입니다.
인터섹션 옵저버 사용법
공식문서에서 자세한 사용법을 확인할 수 있습니다.
IntersectionObserverEntry는 Intersection 옵저버의 콜백 함수 내에서 각 관찰 대상 요소의 가시성 정보를 담고 있는 객체이며, 가장 중요한 속성들은 다음과 같습니다.
- isIntersecting: 관찰 대상 요소가 현재 뷰포트 내에 보이는지 여부를 나타냅니다. Boolean 값으로 반환됩니다.
- intersectionRatio: 관찰 대상 요소와 뷰포트의 교차 영역의 비율을 나타냅니다.이 값은 0부터 1까지의 범위를 가지며, 0은 교차 영역이 없음을, 1은 관찰 대상 요소가 완전히 뷰포트 내에 있음을 의미합니다.
- threshold: 관찰 대상이 지정된 값을 충족했을 때 콜백 함수를 실행하는 기준 값을 의미합니다. 대상의 가시성이 임계값을 충족할 때마다 보고를 받을 수 있습니다.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 요소가 화면에 나타났을 때 애니메이션 적용
entry.target.classList.add('animated');
// 한 번만 애니메이션을 적용하고 싶다면
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.8 // 뷰포트에서 요소의 80%가 보이면 콜백 실행
});
// 관찰할 요소에 observer 등록
observer.observe(elementRef.current);
재렌더링 방지 기법
IntersectionObserver를 사용할 때의 재렌더링 방지 방법
const boxRef = useRef<HTMLDivElement>(null);
const [isAnimated, setIsAnimated] = useState<boolean>(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isAnimated) {
setIsAnimated(true);
// 애니메이션 적용 로직
}
});
if (boxRef.current) {
observer.observe(boxRef.current);
}
return () => {
if (boxRef.current) {
observer.unobserve(boxRef.current);
}
};
}, [isAnimated]); // isAnimated가 변경될 때만 useEffect 재실행
이 방식에서 isAnimated
상태를 사용해 이미 애니메이션이 적용된 컴포넌트는 재실행되지 않도록 최적화했습니다. 의존성 배열에 isAnimated
를 포함시켜 상태 변화를 추적합니다.
2. Motion(Framer Motion)
다운로드
애니메이션 구현에 사용되는 유명한 라이브러리들 중 Top3 안에 드는 라이브러리입니다.
npm i framer-motion
사용법
Framer Motion은 React에 특화된 애니메이션 라이브러리로, 복잡한 애니메이션을 선언적인 방식으로 간단하게 구현할 수 있습니다. whileInView
속성을 사용하면 스크롤 애니메이션을 쉽게 구현할 수 있습니다.
공식문서에 자세히 나와있습니다.
<motion.div
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
{/* 콘텐츠 */}
</motion.div>
재렌더링 방지 기법
Framer Motion에서 재렌더링을 방지하는 방법으로 useInView 훅을 사용할 수 있습니다.
const boxRef = useRef(null);
const [isInView, setIsInView] = useState(false);
// useInView 훅 사용
const inView = useInView(boxRef, { once: true });
useEffect(() => {
if (inView && !isInView) {
setIsInView(true);
}
}, [inView, isInView]);
return (
<motion.div
ref={boxRef}
initial={{ opacity: 0, y: 100 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 100 }}
transition={{ duration: 0.8 }}
>
{/* 콘텐츠 */}
</motion.div>
);
이 접근법에서는 useInView
훅을 사용해 요소의 가시성을 감지하고, 상태(isInView
)를 단 한 번만 변경함으로써 불필요한 재렌더링을 방지합니다. once: true
옵션을 사용하면 요소가 한 번 보인 후에는 더 이상 감지하지 않습니다.
useInView | Motion for React (prev Framer Motion)
A simple state hook for detecting when an element is within the viewport.
motion.dev
3. GSAP
다운로드
GSAP(GreenSock Animation Platform)은 강력한 타임라인 기반 애니메이션 라이브러리로, ScrollTrigger 플러그인을 통해 스크롤 기반 애니메이션을 구현할 수 있습니다.
사용법
GSAP의 핵심 메서드를 사용하면 원하는 요소를 애니메이션화할 수 있습니다.
import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const Box = ({ index }) => {
const boxRef = useRef(null);
useEffect(() => {
const box = boxRef.current;
// 초기 상태 설정
gsap.set(box, { opacity: 0, y: 100 });
// 스크롤 트리거 생성
const st = ScrollTrigger.create({
trigger: box,
start: 'top bottom-=100', // 요소 상단이 뷰포트 하단에서 100px 위에 도달했을 때
end: 'top center', // 요소 상단이 뷰포트 중앙에 도달했을 때
onEnter: () => {
// 요소가 화면에 들어올 때 애니메이션
gsap.to(box, {
opacity: 1,
y: 0,
duration: 0.8,
ease: 'power2.out'
});
},
onLeaveBack: () => {
// 요소가 화면에서 위로 빠져나갈 때 애니메이션
gsap.to(box, {
opacity: 0,
y: 100,
duration: 0.8
});
},
markers: false // 디버깅용 마커 (개발 시에만 true로 설정)
});
// 컴포넌트 언마운트 시 ScrollTrigger 정리
return () => st.kill();
}, [index]); // index 의존성을 통해 각 박스마다 독립적인 ScrollTrigger 인스턴스 생성
return (
<div ref={boxRef} className="box">
<img src={`https://picsum.photos/400?random=${index}`} alt={`random ${index}`} width="400px" height="auto" />
</div>
);
};
GSAP의 ScrollTrigger는 CSS 애니메이션보다 더 세밀한 제어가 가능합니다. start
와 end
속성을 통해 트리거 지점을 정확하게 설정할 수 있으며, 다양한 콜백 함수(onEnter
, onLeave
, onEnterBack
, onLeaveBack
)를 활용해 스크롤 방향에 따른 애니메이션을 구현할 수 있습니다.
디버깅 기능 제공
- markers: 디버깅용 표시기 활성화 가능 (시작/종료 위치를 시각적으로 표시해줍니다.)
- 콘솔에서 ScrollTrigger.getAll()로 모든 인스턴스 확인 가능
재렌더링 방지 기법
GSAP에서 재렌더링을 방지하는 방법으로는, ref와 스크롤 트리거 관리입니다.
const boxRef = useRef(null);
useEffect(() => {
const box = boxRef.current;
// 스크롤 트리거 설정
const st = ScrollTrigger.create({
trigger: box,
// 트리거 설정...
onEnter: () => {
// 애니메이션 로직...
}
});
// 정리 함수를 통한 메모리 누수 방지
return () => {
st.kill();
};
}, [index]); // index만 의존성 배열에 포함해 불필요한 재생성 방지
useRef
를 사용해 DOM 요소에 직접 접근- 의존성 배열에 필요한 값만 포함 (위 예시에서는
index
) - 컴포넌트 언마운트 시
st.kill()
을 호출해 ScrollTrigger 인스턴스 제거 gsap.set()
와gsap.to()
를 사용해 React 상태를 변경하지 않고 직접 DOM 애니메이션
이 방식은 React의 상태 업데이트 없이 DOM 요소를 직접 조작하므로, 불필요한 리렌더링을 방지합니다.
사용한 재렌더링 방지 방법을 정리하면..
1. useRef hook 활용
3가지 방법에서 공통적으로 useRef
를 사용해 DOM 요소에 직접 접근하는 방법을 활용했습니다. 이렇게 하면 DOM 조작이 React의 렌더링 사이클을 우회하여 성능을 향상시킬 수 있습니다.
2. 상태 업데이트 최소화
- IntersectionObserver와 Framer Motion:
isAnimated
또는isInView
상태를 사용해 초기 렌더링 이후 애니메이션이 한 번만 적용되도록 함 - GSAP: React 상태를 사용하지 않고 DOM을 직접 조작해 불필요한 상태 업데이트를 방지
3. 의존성 배열 최적화
각 useEffect의 의존성 배열에 필요한 값(인덱스 등)만 포함시켜 불필요한 효과 재실행을 방지
4. 메모리 관리
컴포넌트 언마운트 시 트리거를 종료하여, 불필요한 메모리 누수를 방지
- IntersectionObserver: observer.unobserve(): 옵저버를 해제
- GSAP: st.kill(): 스크롤 트리거를 종료
lighthouse 비교를 통한 성능 분석
성능 테스트 결과에 따르면 GSAP이 가장 좋은 성능을 보였는데, 이는 GSAP이 React의 가상 DOM 및 렌더링 사이클을 우회하고 직접 DOM을 조작하기 때문인것으로 보입니다.
GSAP과 React의 Virtual DOM 우회 방식 비교
- React의 Virtual DOM과 GSAP의 동작 방식
- React는 Virtual DOM을 활용해 UI를 효율적으로 업데이트합니다.
- GSAP은 Virtual DOM이 아닌 실제 DOM 노드에 직접 접근하여 애니메이션을 적용합니다.
- ref를 통한 직접 DOM 접근
- GSAP을 React에서 사용하려면 useRef 또는 클래스 컴포넌트의 ref를 활용해야 합니다.
- ref.current를 통해 특정 DOM 요소를 참조한 뒤 GSAP 애니메이션 메서드를 호출할 수 있습니다.
- React 렌더링 사이클과 독립적인 동작
- GSAP은 React의 렌더링 사이클에 영향을 받지 않고 독립적으로 동작합니다.
- useGSAP() 같은 커스텀 훅을 사용하면 애니메이션을 React 라이프사이클과 분리하여 실행할 수 있습니다.
- React(선언적) vs GSAP(명령적) 접근 방식 차이
- React는 선언적(Declarative) 방식으로 상태 변화를 통해 UI를 업데이트합니다.
- GSAP은 명령적(Imperative) 방식으로 직접 DOM 요소의 스타일을 변경합니다.
- React가 상태 변화를 통해 DOM을 업데이트한 이후, GSAP이 애니메이션을 적용합니다.
Ref.
각 라이브러리 공식문서
'Web > React' 카테고리의 다른 글
[React] 웹 브라우저 최적화 입문 - Lighthouse를 통한 성능 분석 (0) | 2024.10.12 |
---|---|
[Tailwind CSS] React(JavaScript) & Vite + Tailwind CSS + PostCSS & AutoFixer로 프론트엔드 레포를 초기 구성하는 법 (0) | 2024.10.03 |