✨ 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} />