소개 페이지 최종 시안이 나와서 작업을 바로 들어갔다. 중간에 와이어 프레임대로 짜 봤지만 역시 전체를 다 바꿔야 했고 css keyframe으로 하기엔 리소스가 많이 들어서 framer-motion
라이브러리를 사용하기로 했다.
npm install framer-motion
yarn add framer-motion
/**
* 상수 데이터
* @description 키오스크 스크롤에 따른 애니메이션 효과를 위한 상수
*/
export const SCROLL_THRESHOLDS = [1200, 1900, 2600];
...
// 사용된 컴포넌트
const KioskVideo = () => {
const [index, setIndex] = useState(0);
const [ref] = useInView({ triggerOnce: true, threshold: 0.8 });
useEffect(() => {
const handleScroll = throttle(() => {
for (let i = 0; i < SCROLL_THRESHOLDS.length; i++) {
if (window.scrollY > SCROLL_THRESHOLDS[i]) setIndex(i);
}
}, 200);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section className={styles.kioskBox}>
<div className={styles.stickyBox}>
<TransitionBox index={index} />
<div className={styles.titleBox} ref={ref}>
<TransitionText className={styles.t1} text={MAIN_TITLES[index]} index={index} />
<div className={styles.subTitle}>
<TransitionText text={FIRST_CAPTIONS[index]} index={index} />
<TransitionText text={SECOND_CAPTIONS[index]} index={index} />
</div>
</div>
</div>
</section>
);
};
마우스 스크롤 y축은 콘솔에 찍어보며 계산했고 동적으로 텍스트를 변경해주기 위해 for loop으로 스크롤을 순회하면서 인덱싱 처리를 했다. (텍스트와 이미지 배열 순서를 맞춤)
그리고 기존에 사용하던 react-intersection-observer
의 useInView
를 활용하여 화면에 보여질 때, 이벤트가 발동되게 걸어놨다. (메모리 누수 방지를 위해 이벤트 제거는 필수)
throttle은 스크롤을 할 때 마다, 이벤트가 일어나서 스크롤 함수에 적용 시켜서 일정 시간 간격으로 제한을뒀다.
예를 들면, 이벤트가 일어날 때 마다 실행이 되면 일정 시간 동안 이전에 실행한 함수가 있으면 그 함수의 결과를 반환하고 그렇지 않으면 새로운 함수를 실행하는 기법이다.
(2초 마다 한 번씩만 실행되고 그 사이에는 이전에 실행한 함수 결과를 반환. 결국 스크롤이 빈번하게 일어나도 실행 횟수를 제한할 수 있음)
const TransitionBox = (props: Props) => {
const { index } = props;
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 1 }}>
{IPHONES.map((iphone, i) => (
<motion.div
className={styles.iphoneBox}
key={iphone.id}
initial={{ opacity: i === index ? 1 : 0 }}
animate={{ opacity: i === index ? 1 : 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
>
{i === index && <Image src={iphone.src} width={370} height={1000} alt={iphone.alt} priority />}
</motion.div>
))}
</motion.div>
);
};
const TransitionText = (props: Props) => {
const { text, index, className } = props;
return (
<motion.p
className={className}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
key={index}
transition={{ duration: 0.5 }}
>
{text}
</motion.p>
);
};
실직적으로 애니메이션은 TransitionBox
TransitionText
컴포넌트에서 이루어진다.