Framermotion

framerMotion

⚠️ React 18 이상 버전이 필요합니다.

📑 Summary

Framer Motion은 단순하면서도 강력한 React용 모션 라이브러리입니다. 애니메이션과 상호 작용을 지원합니다.

🤔 어떻게 사용할 수 있을까요?

Framer Motion의 핵심 요소인 <motion />을 이용하여 애니메이션 기능을 추가할 수 있습니다.

기존에 사용하던 태그에서 motion만 추가하면 됩니다.

ex) <div /> => <motion.div />

<motion.div />

구성요소에 애니메이션을 적용하는 것은 다음과 같이 아주 간단하게 할 수 있습니다.

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

⚙️ 설치

npm install framer-motion
---
yarn add framer-motion
---
pnpm add framer-motion

💼 가져오기

설치한 후에는 framerMotion 를 통해 motion을 가져올 수 있습니다

import { motion } from 'framer-motion';

Animation

한 요소의 모든 움직임에 대해서 정의를 합니다.

🪄 애니메이션을 적용하는 방법

<motion.*/> 앞서 보았듯 컴포넌트에 animate 속성을 적용을 해줄 경우 애니메이션을 적용할 수 있습니다.

framerMotion으로 만들어진 요소가 애니메이션을 작동하는 순간은 해당 컴포넌트가 렌더링 됐을 경우입니다. 때문에 아래 코드와 같이 만약 해당 애니메이션 컴포넌트가 반복해서 보이는 경우 매번 애니메이션을 시도하게 됩니다.

import { motion } from 'framer-motion';
import { useState } from 'react';
 
export default function HideButton() {
  const [isVisible, setIsVisible] = useState(true);
  return (
    <div className="flex h-20 w-20 flex-col items-center justify-center">
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? 'hide div' : 'show div'}
      </button>
      // 버튼이 새로 보여질 때마다 돌아갑니다.
      {isVisible && (
        <motion.div
          // 1초동안 270deg 만큼 돌아갑니다.
          animate={{ rotate: '270deg', transition: { duration: 1 } }}
          className="h-10 w-10 rounded-xl bg-primary-200"
        />
      )}
    </div>
  );
}

🙅‍♂️애니메이션을 적용하지 않는 방법

렌더링 시에 애니메이션이 곧바로 적용이 되기 때문에 이를 원치 않을 수도 있습니다.

inital 속성을 이용하여 방지할 수 있습니다. 다음과 같이 작성하면 됩니다.

export default function HideButton() {
  const [isVisible, setIsVisible] = useState(true);
  return (
    <div className="flex h-20 w-20 flex-col items-center justify-center">
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? 'hide div' : 'show div'}
      </button>
      {isVisible && (
        <motion.div
          animate={{ rotate: '270deg', transition: { duration: 1 } }}
          className="h-10 w-10 rounded-xl bg-primary-200"
          inital={false}
        />
      )}
    </div>
  );
}

Transition

한 상태에서 다른 상태로의 움직이는 방식을 정의합니다.

👯‍♀️초기 상태에서 다른 상태로의 움직이는 방식을 정의하는 방법

type과 여러 가지 요소들로 정의를 할 수 있습니다.

type의 종류는 다음과 같습니다.

  • Tween: 기본값으로 설정되어 있는 속성입니다. 이는 CSS 애니메이션과 유사한 동작을 가지고 있습니다. 일정한 속도로 움직이며, 효과가 없는 일반적인 애니메이션을 의미합니다.
  • Spring: 탄성을 가지는 속성입니다. 애니메이션의 끝에 도달했을 때 조금 더 부드럽게 효과를 마무리하는 효과를 줍니다.
  • Inertia: 관성과 관련된 속성입니다. 이 속성을 사용하면 애니메이션이 멈추는 동안 관성의 영향을 받아 부드럽게 멈추는 효과를 줄 수 있습니다. 더 자연스러운 애니메이션 효과를 구현할 수 있습니다.

이외의 요소들은 다음과 같습니다.

  • ease: 애니메이션의 움직임(타이밍)을 조절하는 속성입니다.

    • easeIn: 느린 속도로 시작하고, 점점 빠르게 가속하는 속성입니다.
    • easeInOut: 시작과 끝에서 느린 속도로 진행하고, 중간 지점에서 가장 빠르게 가속하는 속성입니다.
    • easeOut: 시작 시간에 빠른 속도로 시작하고, 점점 느리게 감속하는 속성입니다.
    • linear: 일정한 속도로 진행하는 선형 속성입니다.
    • anticipate: 시작 전에 약간의 후퇴 동작이 있는 속성으로, 예상치 못한 움직임을 만들어냅니다.
  • delay: 처음 애니메이션이 시작할 때까지의 지연 시간을 결정합니다.

  • stagger: 요소 그룹에 적용되는 애니메이션 간격을 설정합니다. 지연 시간을 조절하여 각 요소끼리 일정한 간격으로 애니메이션이 되도록 설정할 수 있습니다.

  • duration: 몇초에 걸쳐 애니메이션이 이루어질지 결정합니다. (default: 3)

  • repeat: 애니메이션의 반복횟수를 결정합니다. Infinity를 사용하여 무한으로 반복할 수도 있습니다.

  • repeatType:

    • loop : 애니메이션이 끝나면 다시 처음부터 재생합니다.
    • reverse : 반복 할 때 마다 역방향으로 재생합니다.
    • mirror : 순방향과 역방향으로 번갈아가면서 재생합니다. 이는 왕복 효과를 만들고자 할 때 유용합니다.
  • repeatDelay: 다음 반복이 시작될 때까지의 지연시간을 설정합니다.

Gestures

<motion/>은 다양한 제스처 인식기(Gestures)를 통해서 React의 이벤트 시스템을 확장합니다.

👏 사용자의 제스처에 따라 애니메이션을 적용하는 방법

다음과 같은 제스처 인식기의 기능들을 제공합니다.

  • whileHover : 컴포넌트 위에 마우스를 올려두었거나 떠날때의 애니메이션

  • whileTap : 컴포넌트를 클릭했을 경우의 애니메이션

  • whileDrag : 컴포넌트를 끌기 제스처가 발생했을 경우의 애니메이션

  • whileFocus : 컴포넌트에 초점을 맞췄을 경우의 애니메이션

  • whileInView : 컴포넌트를 사용자의 뷰포트 안에 들어왔을 경우

    <motion.button
      initial={{ opacity: 0.6 }}
      whileHover={{
        scale: 1.2,
        transition: { duration: 1 },
      }}
      whileTap={{ scale: 0.9 }}
      whileInView={{ opacity: 1 }}
    />

해당 버튼은 초깃값으로 opacity: 0.6의 값을 가지고 있습니다. 만약 커서를 올려두었을 경우 hover 이벤트가 발생해 크기가 scale : 1.2만큼 커집니다. 만약에 누르고 있을 경우에는 scale: 0.9만큼 작아집니다. 또한 화면에 있을 경우 opacity: 1만큼 보이는 것을 알 수 있습니다.

KeyFrame

⏰ 시간에 따라서 원하는 모습을 보여주고 싶은 경우 적용하는 방법

animate의 값을 배열로 설정하면 Motion이 각 값을 차례로 처리합니다. 현재 값을 초기 키 프레임으로 사용하고 싶다면 null 값을 주면 됩니다. 이렇게 하면 애니메이션 되는 도중에 애니메이션이 시작되더라도 전환이 자연스러워집니다

export default function App() {
  return (
    <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: 0,
      }}
    />
  );
}

🫡 팁

만약 시간이 정해져 있지 않을 경우 framerMotion이 알아서 값들을 균등하게 배치해 줍니다.

🚨 주의사항

만약 시간이 정해져 있을 경우 모든 배열의 크기가 동일해야 합니다. 또 초기 상태로 돌아와야지 자연스럽게 infinity 반복도 가능합니다.

times: [0, 0.2, 0.5, 0.8, 1]
borderRadius: ['0%', '0%', '50%', '50%', '0%']
rotate: [0, 0, 180, 180, 0]
scale: [1, 2, 2, 1, 1]

Variants

👪 단일 객체만이 아닌 파생되거나 차례로 이루어지는 애니메이션을 설정하고 싶은 경우

variants props를 활용하면 하위 요소에 전파되는 미리 정의하는 애니메이션을 만들 수도 있습니다.

const isVisible = {
  hide: { opacity: 0 },
  show: { opacity: 1 }
}
 
<motion.div variants={isVisible} />

적용할 애니메이션 animate 속성을 variants 객체에 있는 속성 이름으로 지정하면 됩니다.

사용을 할 때는 다음과 같이 사용을 할 수 있습니다.

<motion.div
  initial="hide"
  animate="show"
  variants={isVisible}
/>
 
<motion.button
  initial="hide"
  animate="show"
  variants={isVisible}
/>

variants를 통해 구독을 한다고 생각하고 initial, animate 각각의 값에 알맞은 속성 값을 입력하여 미리 준비되어 있는 값들을 전달해 줄 수 있는 것입니다.

export default function HideButton() {
  const [isVisible, setIsVisible] = useState(true);
 
  const visible = {
    hide: { opacity: 0 },
    show: { opacity: 1 },
    exit: { x: -200, opacity: 0 },
  };
 
  return (
    <div>
      <motion.button onClick={() => setIsVisible(!isVisible)} layout>
        {isVisible ? 'hide div' : 'show div'}
      </motion.button>
      <AnimatePresence>
        {isVisible && (
          <motion.ul
            initial="hidden"
            animate="visible"
            exit="exit"
            variants={visible}
            transition={{ duration: 1 }}
          >
            <motion.li>JavaScript</motion.li>
            <motion.li>TypeScript</motion.li>
            <motion.li>JavaScript</motion.li>
          </motion.ul>
        )}
      </AnimatePresence>
    </div>
  );
}

위와 같은 경우 <motion.li/>는 부모 태그의 exit를 상속받아 왼쪽으로 사라지게 됩니다.

하지만 다음과 같이 선언한 경우 li는 부모 태그의 상속을 받지 않을 수도 있습니다.

const children = {
  hide: { opacity: 0 },
  show: { opacity: 1 },
  exit: { x: 200, opacity: 0 },
};
 
return (
  <div className="flex flex-col items-center justify-center">
    <motion.button onClick={() => setIsVisible(!isVisible)} layout>
      {isVisible ? 'hide div' : 'show div'}
    </motion.button>
    <AnimatePresence>
      {isVisible && (
        <motion.ul
          initial="hidden"
          animate="visible"
          exit="exit"
          variants={visible}
          transition={{ duration: 1 }}
        >
          <motion.li exit="exit" variants={children}>
            JavaScript
          </motion.li>
          <motion.li exit="exit" variants={children}>
            TypeScript
          </motion.li>
          <motion.li exit="exit" variants={children}>
            JavaScript
          </motion.li>
        </motion.ul>
      )}
    </AnimatePresence>
  </div>
);

이를 통해 부모로부터 전달되는 속성이 있더라도 자식에게 적용이 되어있는 variants가 우선이 된다는 것을 알 수 있습니다.

Animate Presence

😶‍🌫️ 사라지는 컴포넌트에 대해서 애니메이션을 적용하는 방법

리액트를 다뤄본다면 컴포넌트가 보일 때의 애니메이션을 주기는 쉽지만 사라질 때 애니메이션을 주는 것이 쉽지 않습니다. framerMotion에서는 해당 부분을 지원을 하고 있습니다.

<AnimatePresence/>해당 컴포넌트로 감싸주면 사라질 경우에도 애니메이션을 줄 수 있습니다.

<AnimatePresence>
  {isVisible && (
    <motion.div
      // 해당 컴포넌트는 처음에 투명도가 0입니다.
      initial={{ opacity: 0, rotate: null }}
      // 렌더링이 되고난 뒤에 투명도가 1로 바뀝니다.
      animate={{ rotate: '270deg', opacity: 1, transition: { duration: 1 } }}
      // 사라질 경우 다시 투명도가 0이 됩니다.
      exit={{ opacity: 0, rotate: '-270deg' }}
      className="h-10 w-10 rounded-xl bg-primary-200"
    />
  )}
</AnimatePresence>

🚨 TroubleShooting

  • style 값이 적용이 안되는 경우

    motion 컴포넌트가 처음 생성될 때, animate 속성에 적용된 값이 style 또는 inital 에 정의된 값과 다르다면 animate 속성에 적용된 값으로 자동으로 애니메이션을 적용해 줍니다. 자동으로 적용하길 원치 않는다면 inital 값을 false로 설정을 해야합니다.

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

🔎 References