웹에서 애니메이션을 다뤄보자 💃🕺 (2): React에서 스크롤에 따른 애니메이션 구현하기

Ko Seoyoung·2021년 5월 16일
10
post-thumbnail

2️⃣ VanillaJS 코드

두번째로는 내가 구현하고자하는 코드가 vanillaJS로는 어떻게 동작하는지 확인하고 이해해보았다. 참고링크

  • 동작 요약:
  1. 애니메이션이 작동할 div에 특정 클래스 이름(ex) .js-scroll)을 붙여준다.
  2. window scroll event를 통해 스크롤이 발생할 때마다 해당 element들이 viewport안에 들어가는지 검사한다.
  3. 만약 element가 viewport에 포함된다면 애니메이션이 실행되는 css를 갖는 특정 클래스(ex) .scrolled)를 해당 element에 붙여주고 viewport 영역에 벗어나면 특정 클래스를 제거한다.
  • viewport에 포함되는지 검사하는 코드
const elementInView = (el) => {
  const elementTop = el.getBoundingClientRect().top;
 
  return (
    elementTop <= (window.innerHeight || document.documentElement.clientHeight)
  );
};

3️⃣ React 코드로 구현해보기

vanilajs코드를 리액트로 바꿀때 몇가지 의문점이 있었고,
첨부한 자료들을 통해 나만의 해결방안을 찾을 수 있었다!

1. react에서 window scroll event를 어떻게 달아줄까?


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);
    };
  }, []);
};

2. element가 viewport 영역에 해당하는지 확인하는 로직을 react에서 어떻게 구현할까?

핵심은 요소를 참조하는 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;
};

3. styled-component로 keyframe을 어떻게 구현할까?

styled-component를 사용한다면 아래링크를 참고하면 된다!


4️⃣ 컴포넌트로 만들어 재사용 가능하게 만들기

최종 구현한 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};
    `;
  }}
`;
profile
Web Frontend Developer 👩🏻‍💻 #React #Nextjs #ApolloClient

0개의 댓글