Framer motion에서 애니메이션을 주는 방법에 대해서 알아봅니다.
Framer motion은 간단한 애니메이션부터 복잡도가 높은 부분들까지 애니메이션을 주기 위한 다양한 단계별 방법을 지원합니다.
대부분의 애니메이션은 다음과 같이 motion
컴포넌트와 animate
prop으로 동작합니다.
<motion.div animate={{ x: 100 }} />
animate
prop의 값이 달라지는 순간 해당 컴포넌트는 자동으로 업데이트된 값을 반영하여 애니메이트하게 됩니다.
Framer motion은 애니메이션 값 유형에 따라 디폴트로 트랜지션 타입이 정해집니다. 예를 들어, x, y값과 같은 물리적 위치값이 변하는 애니메이션이나 scale
이 변하는 애니메이션의 경우 'spring' 타입 트랜지션이 디폴트로 적용됩니다. 반면에 opacity
나 color
값이 변하는 애니메이션은 'tween' 타입의 트랜지션이 적용됩니다.
물론 개발자가 직접 트랜지션 타입과 시간 등을 다음과 같이 설정할 수 있습니다.
<motion.div
animate={{ x: 100 }}
transition={{ ease: "easeOut", duration: 2 }}
/>
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} />
initial
이 false
이므로 최초 애니메이션은 재생되지 않습니다. 리액트에서 컴포넌트가 DOM 트리에서 제거될 때는 바로 제거되기 때문에 제거될 시 애니메이션을 재생하는것이 굉장히 까다롭습니다. 하지만 Framer motion이 제공하는 AnimatePresence component
를 사용하면 언마운트시에도 애니메이션을 적용할 수 있습니다.
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
animate
prop에는 일련의 keyframe들이 들어가도 됩니다. 시퀀스에 따라 각 값들을 거치면서 애니메이션이 동작하게 됩니다.
<motion.div animate={{ x: [0, 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
}}
/>
- 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] }}
/>
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 }}
/>
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}
/>
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>
)
기본적으로 모든 애니메이션은 정해진 순서에 따라 시작되지만, variants를 사용하면 when, delayChildren, staggerChildren과 같은 추가적인 트랜지션 속성을 지정할 수 있어서 부모 컴포넌트로 하여금 자식 컴포넌트들의 애니메이션을 오케스트레이션 할 수 있게 해 줄 수 있습니다.
const list = {
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.3,
},
},
hidden: {
opacity: 0,
transition: {
when: "afterChildren",
},
},
}
개발자가 정의한 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
(인덱스)를 전달하는 것을 확인할 수 있습니다.
위에서 확인한 예제들에서는 whileHover
나 animate
같은 prop들이 하나의 variant값만을 참조하지만, 사실 여러 variant들을 참조하게 할 수 있습니다. 다음과 같이 단순히 variant의 이름들이 담긴 배열을 넣어주면 됩니다.
<motion.ul variants={["open", "primary"]} />
만약 위에서 전달해준 open
와 primary
라는 variant들이 같은 css속성을 각각 다르게 정의하고 있으면 나중에 위치한 primary
variant가 우선 적용되게 됩니다.
대부분의 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
훅은 중요한 훅이라 다음에 단일 포스팅을 통해 더 자세히 다뤄볼 예정입니다!
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
})
현재 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>
위 코드는 아래와 같이 애니메이션이 들어간 값을 렌더링하게 됩니다.
브라우저들은 몇몇의 애니메이션들은 GPU를 사용해서 재생할 수 있습니다. 이렇게 GPU를 사용하면 에너지 효율도 좋고, CPU를 이용하는 경우보다 훨씬 부드럽게 재생됩니다.
하지만 대부분의 브라우저에 내장된 API들은 Framer motion의 자바스크립트로 작성된 애니메이션보다 기능이 적습니다. Framer motion이 사용하는 엔진은 애니메이션이 언제 GPU로 안전하게 재생될 수 있는지 판단하는 기능도 제공합니다. 심지어 전통적으로 GPU에서 재생할 수 없는 애니메이션들(스프링이나 커스텀 이징 함수들)또한 지원해줍니다.
아래에 명시된 경우가 하나라도 존재하면 위 속성값들 또한 GPU를 사용하여 재생할 수 없습니다.
motion
컴포넌트가 onUpdate
prop을 가지는 경우MotionValue
가 style
prop으로 전달된 경우repeatDelay
가 설정된 경우repeatType
이 "mirror"
로 설정된 경우damping
이 0
으로 설정된 경우