
두번째로는 내가 구현하고자하는 코드가 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};
    `;
  }}
`;