두번째로는 내가 구현하고자하는 코드가 vanillaJS로는 어떻게 동작하는지 확인하고 이해해보았다. 참고링크
const elementInView = (el) => {
const elementTop = el.getBoundingClientRect().top;
return (
elementTop <= (window.innerHeight || document.documentElement.clientHeight)
);
};
vanilajs코드를 리액트로 바꿀때 몇가지 의문점이 있었고,
첨부한 자료들을 통해 나만의 해결방안을 찾을 수 있었다!
function HomePage({ location }) {
const handleScrollAnimation = (e) => {
console.log(e);
};
useEffect(() => {
window.addEventListener('scroll', (e) => {
handleScrollAnimation(e);
});
return () => {
window.removeEventListener('scroll', (e) => {
handleScrollAnimation(e);
});
};
}, []);
return (
<MainLayout location={location}>
<AnimationTest wrapperStyle={{ backgroundColor: GREY[500] }} />
<AnimationTest wrapperStyle={{ backgroundColor: GREY[300] }} />
<AnimationTest wrapperStyle={{ backgroundColor: GREY[700] }} />
</MainLayout>
);
}
export default HomePage;
스크롤할 때마다 event object가 콘솔창에 찍히는 것을 확인할 수 있다
이 로직은 자주 사용될 것 같아서 useWindowScrollEvent 커스텀 훅으로 만들었다.
import { useEffect } from 'react';
export const useWindowScrollEvent = (
listener: (this: Window, ev: Event) => any
) => {
useEffect(() => {
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
}, []);
};
핵심은 요소를 참조하는 ref를 사용하는 것이다!
ref.current가 참조하는 element이다.
import React, { CSSProperties, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { useWindowScrollEvent } from '@src/hooks/useWindowScrollEvent';
import { checkIsInViewport } from '@src/lib/utils/viewport';
function AnimationTest() {
const [animation, setAnimation] = useState(true);
const areaRef = useRef<HTMLParagraphElement>();
const handleScrollAnimation = () => {
const elementTop = areaRef?.current?.getBoundingClientRect().top;
setAnimation(checkIsInViewport(elementTop));
};
useWindowScrollEvent(handleScrollAnimation);
return (
<Wrapper>
<Text ref={areaRef} animation={animation}>
Testing Animation...
</Text>
</Wrapper>
);
}
export default AnimationTest;
const Wrapper = styled.div`
height: 40rem;
padding: 1.6rem;
`;
const goup = keyframes`
from { transform: translateY(5rem); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
`;
const Text = styled.p<{
animation: boolean;
}>`
${({ animation }) => !animation && 'transform: translateY(5rem); opacity: 0;'}
animation: ${({ animation }) => animation && goup} 2s ease-out;
font-weight: bold;
font-size: 2rem;
`;
cf. element가 viewport안에 있는지 확인하는 로직도 모듈화 해주었다.
export const checkIsInViewport = (elem: HTMLElement) => {
if (!elem || !window) {
return false;
}
const {
top: elementTop,
bottom: elementBottom,
} = elem.getBoundingClientRect();
return elementBottom > 0 && elementTop <= window.innerHeight;
};
styled-component를 사용한다면 아래링크를 참고하면 된다!
최종 구현한 ScrollRevealSlideAnimation 컴포넌트
import React, { useEffect, useRef, useState } from 'react';
import styled, { css, keyframes } from 'styled-components';
import { useWindowScrollEvent } from '@src/hooks/useWindowScrollEvent';
import { checkIsInViewport } from '@src/lib/utils/viewport';
export type DirectionType = 'top' | 'bottom' | 'right' | 'left';
export type ScrollRevealSlideAnimationProps = {
children: React.ReactNode;
direction?: DirectionType;
};
function ScrollRevealSlideAnimation({
children,
direction = 'top',
}: ScrollRevealSlideAnimationProps) {
const elemRef = useRef<HTMLDivElement>(null);
const [isInViewPort, setIsInViewPort] = useState(
checkIsInViewport(elemRef?.current)
);
useEffect(() => {
// elemRef이 초기에 값이 바로 들어오지 않아
// elemRef가 undefined가 아닐때 isInViewPort값을 다시 할당한다.
setIsInViewPort(checkIsInViewport(elemRef?.current));
}, [elemRef?.current === undefined]);
// 스크롤이 될때마다 element가 뷰포트 영역 안인지 체크한다.
useWindowScrollEvent(() => {
setIsInViewPort(checkIsInViewport(elemRef?.current));
});
return (
<Wrapper ref={elemRef} isInViewPort={isInViewPort} direction={direction}>
{children}
</Wrapper>
);
}
export default ScrollRevealSlideAnimation;
const Wrapper = styled.div<{
isInViewPort: boolean;
direction: DirectionType;
}>`
${({ isInViewPort, direction }) => {
const axis = direction === 'top' || direction === 'bottom' ? 'Y' : 'X';
const dir = direction === 'bottom' || direction === 'right' ? -1 : 1;
const [translateFrom, translateTo] = [
`translate${axis}(${4 * dir}rem)`,
`translate${axis}(0)`,
];
const defaultStyle = css`
transform: ${translateFrom};
opacity: 0;
`;
const keyframe = keyframes`
from { transform: ${translateFrom}; opacity: 0; }
to { transform: ${translateTo}; opacity: 1; }
`;
const animationRule = css`
${keyframe} 2s ease
`;
// isInViewPort가 true라면
// 방향에 따라 translate(이동) 애니메이션을 실행한다.
return css`
${!isInViewPort && defaultStyle}
animation: ${isInViewPort && animationRule};
`;
}}
`;