[FM] Animation - Overview

0

Framer-motion

목록 보기
3/5
post-thumbnail
post-custom-banner

Framer motion에서 애니메이션을 주는 방법에 대해서 알아봅니다.

Animation

Framer motion은 간단한 애니메이션부터 복잡도가 높은 부분들까지 애니메이션을 주기 위한 다양한 단계별 방법을 지원합니다.

Simple Animations

대부분의 애니메이션은 다음과 같이 motion 컴포넌트와 animate prop으로 동작합니다.

<motion.div animate={{ x: 100 }} />

animate prop의 값이 달라지는 순간 해당 컴포넌트는 자동으로 업데이트된 값을 반영하여 애니메이트하게 됩니다.

Transitions

Framer motion은 애니메이션 값 유형에 따라 디폴트로 트랜지션 타입이 정해집니다. 예를 들어, x, y값과 같은 물리적 위치값이 변하는 애니메이션이나 scale이 변하는 애니메이션의 경우 'spring' 타입 트랜지션이 디폴트로 적용됩니다. 반면에 opacitycolor값이 변하는 애니메이션은 'tween' 타입의 트랜지션이 적용됩니다.
물론 개발자가 직접 트랜지션 타입과 시간 등을 다음과 같이 설정할 수 있습니다.

<motion.div
  animate={{ x: 100 }}
  transition={{ ease: "easeOut", duration: 2 }}
/>

Enter animations

Enter animation 이란, motion 컴포넌트가 화면에 처음 생성될 때 동작하는 애니메이션입니다. animate prop의 값이 style 이나 initial prop에 있는 값과 다르다면 첫 마운트 후 바로 animate prop값을 반영하여 애니메이션이 작동하게 되는것입니다. 이 Enter animation을 없애고 싶다면 initial prop에 false값을 넣으면 됩니다.

<motion.div
  className="box"
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
/>
  • 투명도가 initial에는 0, animate에는 1로 값이 다르기 때문에 div가 첫 마운트될 시 투명도가 0에서 1로 바뀌는 애니메이션이 재생됩니다.
<motion.div animate={{ x: 100 }} initial={false} />
  • initialfalse이므로 최초 애니메이션은 재생되지 않습니다.

Exit animations

리액트에서 컴포넌트가 DOM 트리에서 제거될 때는 바로 제거되기 때문에 제거될 시 애니메이션을 재생하는것이 굉장히 까다롭습니다. 하지만 Framer motion이 제공하는 AnimatePresence component를 사용하면 언마운트시에도 애니메이션을 적용할 수 있습니다.

<AnimatePresence>
  {isVisible && (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  )}
</AnimatePresence>

Keyframes

animate prop에는 일련의 keyframe들이 들어가도 됩니다. 시퀀스에 따라 각 값들을 거치면서 애니메이션이 동작하게 됩니다.

<motion.div animate={{ x: [0, 100, 0] }} />
  • div가 x축으로 100만큼 갔다가 다시 0의 위치로 오는 애니메이션입니다.
   <motion.div
      className="box"
      animate={{
        scale: [1, 2, 2, 1, 1],
        rotate: [0, 0, 180, 180, 0],
        borderRadius: ["0%", "0%", "50%", "50%", "0%"]
      }}
      transition={{
        duration: 2,
        ease: "easeInOut",
        times: [0, 0.2, 0.5, 0.8, 1],
        repeat: Infinity,
        repeatDelay: 1
      }}
    />
  • keyframe을 이용한, div가 회전함과 동시에 크기가 2배만큼 커지면서 모양이 정사각형에서 원으로 바뀌었다 돌아가는 조금 복잡한 애니메이션입니다.
  • wildcard keyframe
    와일드카드 키프레임은 키프레임에 null값을 넣는것을 말합니다. null값을 넣게 되면 현재 값(current value)이 그 자리에 적용되게 되는데, 이렇게 와일드카드 키프레임을 사용함으로써 반복을 최소화할 수 있습니다.
<motion.div 
    className="box"
    /**
     * Setting the initial keyframe to "null" will use
     * the current value to allow for interruptable keyframes.
     */
    whileHover={{ scale: [null, 1.5, 1.4] }}
    transition={{ duration: 0.3 }}
  />
<motion.circle cx={500} animate={{ cx: [null, 100] }} />

각 keyframe은 기본적으로 재생 타임라인 안에서 고르게 배치됩니다. 하지만 다음과 같이 transition prop의 times 속성을 지정함으로써 좀더 정교한 조정이 가능합니다.

<motion.circle
  cx={500}
  animate={{ cx: [null, 100, 200] }}
  transition={{ duration: 3, times: [0, 0.2, 1] }}
/>
  • times는 keyframe 배열과 같은 길이의 배열로, 각 요소는 0과 1사이의 값들로 이루어져 있습니다. keyframe 배열에 들어가는 값들은 times 배열에 자신의 인덱스와 같은 인덱스에 위치한 요소에 있는 값으로 애니메이션 타임라인상 자신의 위치가 정해지게 됩니다.

Gesture animations

Framer motion에는 hover, tap, drag, focus, inView와 같은 제스쳐들이 시작할 때 애니메이션을 줄 수 있는 좋은 도구들이 있습니다.

<motion.button
  initial={{ opacity: 0.6 }}
  whileHover={{
    scale: 1.2,
    transition: { duration: 1 },
  }}
  whileTap={{ scale: 0.9 }}
  whileInView={{ opacity: 1 }}
/>
  • Framer motion은 제스쳐가 끝났을 때 어떤 상태로 돌아가야 하는지 자동적으로 감지합니다.

Variants

animate prop에 단일 애니메이션 객체를 넣어 애니메이션을 동작시키는 것은 간단하고 편리합니다. 하지만 조금 더 복잡하게 DOM트리를 타고 전파되는 형태의 애니메이션을 넣고 싶을 때는 variants를 통해 복잡한 애니메이션 객체를 사용할 수 있습니다.

  • Variants?
    Variants는 미리 정의된 target들의 집합입니다. 즉, 애니메이션된 상태를 aliasing하는 것이라고 봐도 무방합니다.
const variants = {
  visible: { opacity: 1 },
  hidden: { opacity: 0 },
}

다음과 같이 motion 컴포넌트에 variants라는 prop으로 해당 객체가 들어가게 됩니다.

<motion.div variants={variants} />

또한 다음과 같이 라벨링을 통하여 애니메이션 객체를 정의할 수 있는 모든 곳에서 참조할 수도 있습니다.

<motion.div
  initial="hidden"
  animate="visible"
  variants={variants}
/>

Propagation

motion 컴포넌트가 자식 컴포넌트를 가지면, variant값의 변화는 컴포넌트 계층구조를 따라 자식으로 전파되게 됩니다. 이 때 자식 컴포넌트가 고유한 animate 속성이 있다면 더 이상 전파되지 않습니다.

const list = {
  visible: { opacity: 1 },
  hidden: { opacity: 0 },
}

const item = {
  visible: { opacity: 1, x: 0 },
  hidden: { opacity: 0, x: -100 },
}

return (
  <motion.ul
    initial="hidden"
    animate="visible"
    variants={list}
  >
    <motion.li variants={item} />
    <motion.li variants={item} />
    <motion.li variants={item} />
  </motion.ul>
)

Orchestration

기본적으로 모든 애니메이션은 정해진 순서에 따라 시작되지만, variants를 사용하면 when, delayChildren, staggerChildren과 같은 추가적인 트랜지션 속성을 지정할 수 있어서 부모 컴포넌트로 하여금 자식 컴포넌트들의 애니메이션을 오케스트레이션 할 수 있게 해 줄 수 있습니다.

const list = {
  visible: {
    opacity: 1,
    transition: {
      when: "beforeChildren",
      staggerChildren: 0.3,
    },
  },
  hidden: {
    opacity: 0,
    transition: {
      when: "afterChildren",
    },
  },
}

Dynamic Variants

개발자가 정의한 variant들의 각 속성은 동적으로 작동하게 할 수 있습니다. 정적인 속성값이 아니라 함수로써 정의할 수 있기 때문입니다. 이게 무슨뜻인지 알아보도록 하겠습니다.
variant를 함수로 정의한다는 것은 어떤 곳에서 해당 variant를 접근할 때마다 다른 값으로 resolve되게 할 수 있다는 것입니다.
다음 예제를 보겠습니다.

const variants = {
  visible: i => ({
    opacity: 1,
    transition: {
      delay: i * 0.3,
    },
  }),
  hidden: { opacity: 0 },
}

return items.map((item, i) => (
  <motion.li
    custom={i}
    animate="visible"
    variants={variants}
  />
))

위의 variants 객체의 visible 속성은 함수로 정의되어 있습니다. 파라미터인 i값에 따라 delay속성이 변합니다. 이렇게 함수로 정의된 variant의 아규먼트는 해당 variant를 사용하는 motion 컴포넌트의 custom prop으로 전달 가능합니다. 위 예제에서는 items라는 배열을 돌면서 motion 컴포넌트들을 여러개 생성하고, 각 index의 값만큼 delay를 더 많이 주고 있는 예제입니다. custom prop에 i(인덱스)를 전달하는 것을 확인할 수 있습니다.

Multiple variants

위에서 확인한 예제들에서는 whileHoveranimate같은 prop들이 하나의 variant값만을 참조하지만, 사실 여러 variant들을 참조하게 할 수 있습니다. 다음과 같이 단순히 variant의 이름들이 담긴 배열을 넣어주면 됩니다.

<motion.ul variants={["open", "primary"]} />

만약 위에서 전달해준 openprimary라는 variant들이 같은 css속성을 각각 다르게 정의하고 있으면 나중에 위치한 primary variant가 우선 적용되게 됩니다.

Manual controls

대부분의 UI 인터랙션 상황에서는 애니메이션을 명료하게 선언적으로 넣어주는 것이 best practice이지만, 상황에 따라 복잡한 애니메이션들의 시퀀스를 오케스트레이션해줘야 하는 경우들이 있습니다.
이런 복잡한 애니메이션 시퀀스 오케스트레이션을 도와주는 것이 바로 useAnimate훅입니다.

useAnimate이 사용되는 상황

  • HTML, SVG 엘리먼트들만을 움직여야 할 때
  • 애니메이션 시퀀스가 복잡할 때
  • time, speed, play(), pause()와 같은 playback 컨트롤 요소들을 제어해야 할 때
const MyComponent = () => {
  const [scope, animate] = useAnimate()
  
  useEffect(() => {
    const animation = async () => {
      await animate(scope.current, { x: "100%" })
      animate("li", { opacity: 1 })
    }
    
    animation()
  }, [])

  return (
    <ul ref={scope}>
      <li />
      <li />
      <li />
    </ul>
  )
}

useAnimate훅은 중요한 훅이라 다음에 단일 포스팅을 통해 더 자세히 다뤄볼 예정입니다!

Animate single values

useAnimate훅은 위에서 설명한 여러 애니메이션 시퀀스를 다루는 경우 외에도, 단일 값(MotionValue도 가능)을 위해서도 사용될 수 있습니다.

MotionValue란?
애니메이션 되고 있는 속성값을의 상태나 속도와 같은, 일종의 메타데이터를 추적하기 위한 값입니다. 모든 motion 컴포넌트들이 내부적으로 이 MotionValue를 사용합니다.

const [scope, animate]= useAnimate()
const x = useMotionValue(0)

useEffect(() => {
  const controls = animate(x, 100, {
    type: "spring",
    stiffness: 2000,
    onComplete: v => {}
  })

  return controls.stop
})

Animate content

현재 MotionValue의 값을 motion 컴포넌트의 자식으로 넣어줌으로써 렌더링할 수 있습니다.

const count = useMotionValue(0)
const rounded = useTransform(count, latest => Math.round(latest))

useEffect(() => {
  const controls = animate(count, 100)

  return controls.stop
}, [])

return <motion.div>{rounded}</motion.div>

위 코드는 아래와 같이 애니메이션이 들어간 을 렌더링하게 됩니다.

Hardware-accelerated animations

브라우저들은 몇몇의 애니메이션들은 GPU를 사용해서 재생할 수 있습니다. 이렇게 GPU를 사용하면 에너지 효율도 좋고, CPU를 이용하는 경우보다 훨씬 부드럽게 재생됩니다.
하지만 대부분의 브라우저에 내장된 API들은 Framer motion의 자바스크립트로 작성된 애니메이션보다 기능이 적습니다. Framer motion이 사용하는 엔진은 애니메이션이 언제 GPU로 안전하게 재생될 수 있는지 판단하는 기능도 제공합니다. 심지어 전통적으로 GPU에서 재생할 수 없는 애니메이션들(스프링이나 커스텀 이징 함수들)또한 지원해줍니다.

GPU 가속을 지원하는 속성값들

  • transform
  • opacity
  • clipPath
  • filter
  • background-color

GPU 가속을 지원하지 않는 기능들

아래에 명시된 경우가 하나라도 존재하면 위 속성값들 또한 GPU를 사용하여 재생할 수 없습니다.

  • motion 컴포넌트가 onUpdate prop을 가지는 경우
  • MotionValuestyle prop으로 전달된 경우
  • repeatDelay가 설정된 경우
  • repeatType"mirror"로 설정된 경우
  • damping0으로 설정된 경우
post-custom-banner

0개의 댓글