framer motion



효과는 양쪽에서 다르지만 같은 논리로 슬라이더를 구현해야 했습니다.

원래는 페이지별로 코드를 작성했는데 반복되는 영역이므로 catch로 처리하도록 하겠습니다.

const useSlider = <T>(
  data: T(),
  duration?: number,
  moveScale?: number,
  opacity?: boolean,
  type?: 'spring' | 'linear' | 'tween' | 'inertia',
): (index: number, direction: number, increase: () => void, decrease: () => void, animationVariant: Variants)=>{
...
}

나중에 슬라이더의 애니메이션을 사용자 정의하기 위해 Duration(지속 시간), moveScale(움직이는 정도), Opacity(투명도 변경) 및 Type(애니메이션 효과 유형)을 입력했습니다.

반환되는 결과는 슬라이더의 인덱스, 애니메이션이 진행되는 방향, 증가 함수, 감소 함수, 애니메이션 효과 개체입니다.

useState, 증가, 감소

  const ((index, direction), setIndex) = useState((0, 0));
  function increase() {
    setIndex((prev) => (prev(0) > data.length - 2 ? (0, +1) : (index + 1, +1)));
  }
  function decrease() {
    setIndex((prev) => (prev(0) < 1 ? (data.length - 1, -1) : (index - 1, -1)));
  }

인덱스와 디렉션은 같은 상태로 관리되었습니다. 이는 setState가 비동기적으로 작동하고 애니메이션이 별도의 상태로 관리되어 제대로 작동하지 않았기 때문입니다.

단일 상태 및 콜백 함수에 대해 증가 및 감소 함수를 만들었습니다.

애니메이션 개체

  const animationVariant = {
    initial: (direction: number) => {
      return {
        x: moveScale ? (direction > 0 ? moveScale : -moveScale) : direction > 0 ? 1000 : -1000,
        opacity: opacity && 0,
        transition: {
          duration: duration ? duration : 0.5,
          type: type && type,
        },
      };
    },
    visible: {
      x: 0,
      opacity: opacity && 1,
      transition: {
        duration: duration ? duration : 0.5,
        type: type && type,
      },
    },
    exit: (direction: number) => {
      return {
        opacity: opacity && 0,
        x: moveScale ? (direction < 0 ? moveScale : -moveScale) : direction < 0 ? 1000 : -1000,
        transition: {
          duration: duration ? duration : 0.5,
          type: type && type,
        },
      };
    },
  };

애니메이션은 방향에 따라 결정되기 때문에 실제 움직임이 있어야 하는 initial과 exit를 함수와 반환 객체로 생성했습니다.

전체 코드

import { Variants } from 'framer-motion';
import { useState } from 'react';

const useSlider = <T>(
  data: T(),
  duration?: number,
  moveScale?: number,
  opacity?: boolean,
  type?: 'spring' | 'linear' | 'tween' | 'inertia',
): (index: number, direction: number, increase: () => void, decrease: () => void, animationVariant: Variants) => {
  const ((index, direction), setIndex) = useState((0, 0));
  function increase() {
    setIndex((prev) => (prev(0) > data.length - 2 ? (0, +1) : (index + 1, +1)));
  }
  function decrease() {
    setIndex((prev) => (prev(0) < 1 ? (data.length - 1, -1) : (index - 1, -1)));
  }

  // 애니메이션 효과 관련 코드 initial은 초기(등장시 초기값), visible은 initial에서 랜더링될시까지 보여줄 애니메이션, exit은 visible에서 컴포넌트가 지워질때 보여줄 애니메이션
  const animationVariant = {
    initial: (direction: number) => {
      return {
        x: moveScale ? (direction > 0 ? moveScale : -moveScale) : direction > 0 ? 1000 : -1000,
        opacity: opacity && 0,
        transition: {
          duration: duration ? duration : 0.5,
          type: type && type,
        },
      };
    },
    visible: {
      x: 0,
      opacity: opacity && 1,
      transition: {
        duration: duration ? duration : 0.5,
        type: type && type,
      },
    },
    exit: (direction: number) => {
      return {
        opacity: opacity && 0,
        x: moveScale ? (direction < 0 ? moveScale : -moveScale) : direction < 0 ? 1000 : -1000,
        transition: {
          duration: duration ? duration : 0.5,
          type: type && type,
        },
      };
    },
  };
  return (index, direction, increase, decrease, animationVariant as Variants);
};

export default useSlider;

적용된 코드

부모 구성 요소

const ProjectSlider = () => {
  const (index, direction, increase, decrease, animationVariants) = useSlider<IProjectInner>(
    ProjectData,
    0.5,
    1000,
    true,
    'spring',
  );
  return (
    <Wrapper>
      <Left size={30} onClick={increase} />
      <RelativeWrapper>
        <Project ProjectData={ProjectData(index)} direction={direction} animationVaraints={animationVariants} />
      </RelativeWrapper>
      <Right size={30} onClick={decrease} />
    </Wrapper>
  );
};

하위 구성 요소

const Project = ({
  ProjectData,
  direction,
  animationVaraints,
}: {
  ProjectData: IProjectInner;
  direction: number;
  animationVaraints: Variants;
}) => {
  return (
    <AnimatePresence initial={false} custom={direction}>
      <ProjectWrapper
        variants={animationVaraints}
        initial="initial"
        animate="visible"
        exit="exit"
        key={ProjectData.introduce}
        custom={direction}
      >
      ...
      </ProjectWrapper>
    </AnimatePresence>
  );
};


결과