Framer Motion은 모션 그래픽과 애니메이션을 구현하기 위한 라이브러리이다.
주로 React 애플리케이션에서 사용되며, 다양한 애니메이션 효과를 쉽게 추가할 수 있도록 도와준다.
npm install framer-motion 으로 설치를 진행하면 된다.
위의 코드처럼 import를 한 다음,
motion을 사용할 때에는 <motion.div>와 같이 태그를 작성해야 사용이 가능하다는 점을 꼭 알고 있자!!
여기서 잠깐! 우리는 CSS 속성을 정의할 때, styled-component를 사용했다.
그럼 어떻게 styled-component 랑 motion을 사용할 수 있을 까?

이와 같이 const Box = styled(motion.div) 로 사용하면, motion의 div라는 의미로 styled-component를 적용할 수 있으니 꼭 기억하자!

이렇게 움직이는 박스를 만들어보자
먼저, 처음에 박스가 사라졌다가 회전을 하면서 박스가 생겨나고, 몇초 후에 멈춘다! 라는 것을 알 수 있다.
<Box
initial={{ scale: 0 }} // 초기 상태에서의 스타일과 애니메이션 설정
animate={{ scale: 1, rotateZ: 360 }} // 애니메이션 동작 중의 스타일과 애니메이션 설정
transition={{ type: 'spring', delay: 0.5 }} // 애니메이션 전환 설정
/>
initial: 요소의 초기 상태를 설정
여기서 initial={{ scale: 0 }}는 요소의 초기 스케일(scale)을 0으로 설정한다.
이는 요소가 처음에는 화면에서 보이지 않고 축소된 상태로 시작한다는 것을 의미한다.
animate : 요소가 애니메이션을 통해 어떻게 변화할지 설정한다.
animate={{ scale: 1, rotateZ: 360 }} 는 요소가 애니메이션 동작 중에 확대(scale: 1)되고, 동시에 Z축 기준으로 360도 회전(rotateZ: 360)하는 것을 의미한다.
transition : 애니메이션 전환(transition)을 설정한다.
transition={{ type: 'spring', delay: 0.5 }}는 이 애니메이션이 스프링(Spring) 타입의 애니메이션이며, 0.5초의 지연(delay: 0.5) 후에 시작된다는 것을 나타낸다.
스프링 타입의 애니메이션은 자연스럽고 탄성 있는 움직임을 제공하며, 지연 설정은 애니메이션 시작 전에 대기 시간을 설정하는 것이다.
만약, 애니메이션이 많아지면 컴포넌트 안에 적어야 할 것들이 엄청 많아지면 코드가 복잡해질 수 있다
--> 깔끔하게 코드를 작성하는 방ㅂ덥은 뭐가 있을까?
Framer Motion의 variants 기능을 사용하면 된다
variants는 여러 가지 상태(variants) 를 정의하고, 각 상태에서의 스타일과 애니메이션을 설정할 수 있다.
먼저 상수 myVars정의를 해보자
Box 컴포넌트에 variants={myVars}를 설정하여 애니메이션의 상태를 myVars에서 정의한 상태들로 관리한다.
initial="start" prop을 통해 초기 상태(initial state)를 start로 설정한다. 따라서 요소가 렌더링될 때 초기 스케일(scale: 0)로 시작한다.
animate="end" prop을 통해 애니메이션을 적용할 상태를 end로 설정한다. 이 prop은 요소가 애니메이션 동작 중에는 확대(scale: 1) 및 회전(rotateZ: 360)되도록 지정한다. 또한, transition 설정에 따라 스프링(Spring) 타입의 애니메이션과 0.5초의 지연이 적용된다.
이렇게 하나의 상수에 필요한 animation에 대해서 설정해두고, variant 속성을 이용해 가간편하게 다양한 애니메이션을 구현할 수 있다.

이제 이렇게 움직이는 박스를 만들어 볼 것이다!
간단하게 먼저 생각해보자면
<Box> 컨테이너 안에 <Circle> 컨테이너 4개가 있을 것이다
그리고 animation은 먼저 Box가 등장한 뒤, circle이 한개씩 등장하는 모습이다.

그럼 우선 <Box/>, <Circle/> 컴포넌트에서 사용할 Variant들을 정의해 보자!
1. boxVariants: Box 컴포넌트의 애니메이션 설정
start 상태:
opacity: 0 : 투명도를 0으로 설정하여 요소가 보이지 않게 만든다.
scale: 0.5 : 요소의 크기를 50%로 축소
end 상태:
opacity: 1 : 투명도를 1로 설정하여 요소가 완전히 보이도록 한다.
scale: 1 : 요소의 크기를 원래 크기로 설정한다.
transition (애니메이션 전환 설정):
type: 'spring' : 스프링 기반 애니메이션으로 자연스럽고 탄력적인 효과를 제공한다.
bounce: 0.5 : 스프링 애니메이션의 탄력성을 조절한다. 값이 클수록 더 많이 튀귄다.
duration: 1 : 애니메이션이 1초 동안 진행되도록 설정
delayChildren: 1: 자식 요소의 애니메이션이 시작되기 전에 1초 지연을 적용한다.
staggerChildren: 0.2: 각 자식 요소의 애니메이션이 0.2초 간격으로 시작되도록 설정한다.
여기서 가장 중요하게 살펴봐야 할 것은 delayChildren, staggerChildren 이다!
delayChildren
우리는 현재 Box가 1초 동안 진행되는 것을 알고 있다.
---> 즉, circle은 Box의 animation이 끝나고 animation이 실행되야 한다.
--> Box의 animation (1초)가 끝난 뒤, 자식 요소의 애니메이션을 진행한다! 라는 말을 의미한다고 생각하면 된다.
staggerChildren
circle은 왼쪽 위에 원부터 하나씩 등장한다.
만약 원래라면, 원에다 5초, 1초 1.5초, 2초 씩 delay를 주거나 해야하지만, staggerChildren을 사용하게 되면, 0.2 간격으로 원이 등장하게 된다!
2. circleVariants: Circle 컴포넌트의 애니메이션 설정
start 상태:
opacity: 0 : 투명도를 0으로 설정하여 요소가 보이지 않게 만든다.
y: 10 : 요소를 y축 방향으로 10px 아래로 이동시킨다.
end 상태:
opacity: 1 : 투명도를 1로 설정하여 요소가 완전히 보이도록 한다.
y: 0: 요소를 원래 위치로 되돌아 오게 한다.

어? 근데 왜 <Circle/> 컴포넌트는 initial , animate를 적용하지 않았는데 왜 적용이 되는거지?
부모 컴포넌트인 Box에서
initial="start"와animate="end"를 지정했다.
Box는 자식 컴포넌트인 Circle에게도 이 설정을 전달하게 된다.
Box는variants객체의 상태 전환(start에서 end로 전환)을 수행하며, 이 전환이 Box 내의 모든 자식 컴포넌트(Circle)에도 적용된다.
중요한 내용을 다시 한번 정리해보자
애니메이션 상속의 주요
delayChildren과 staggerChildren :
delayChildren : 부모 컴포넌트(Box)의 애니메이션이 완료된 후 자식 컴포넌트(Circle)의 애니메이션이 시작되는 시간을 지연시킨다. staggerChildren : 자식 컴포넌트들이 순차적으로 애니메이션을 시작하도록 간격을 조절한다. 이를 통해 각 Circle이 0.2초 간격으로 애니메이션을 시작하게 된다.자식 컴포넌트에서의 애니메이션 설정:
자식 컴포넌트 (Circle): initial과 animate 속성을 직접 설정하지 않았지만, 부모 컴포넌트의 initial과 animate 상태 이름(start, end) 을 상속받아 동일한 상태 전환을 따른다.
이번에는 마우스를 올렸을 때, 90도로 회전하고,
클릭했을 때 크기가 줄어들면서~ 원 모양이 되는 형태를 한번 만들어 보겠다
이떄는 이벤트를 적용해야 하는데 (이벤트라고 하면 onClick, hover...이런것들이 생각이 나는데) , motion에서는 whileHover, whileTap으로 사용할 수 있다.

whileHover = {{ scale : 1.5, rotateZ : 90}}
whileTap = {{ scale : 1, borderRadius : '100px' }}
로 정의할 수 있지만, variants의 기능을 사용해서 boxVariants에 hover, tap으로 각각의 animation을 지정해두고, whileHover ="hover", whileTap="tap" 으로 지정해 두었다!
간단하게 설명하자면, hover을 했을 때, 크기는 1.5배에 90도 회전을 하고,
거기서 tap을 했을 떄, 크기는 다시 원래대로 돌아오고, borderRadius를 '100px'를 준다. 라고 생각하면 될 것 같다!
어제까지 열심히 만들었던 toDoBoard에 기능으로 있었던 Drag를 motion에서는 어떻게 적용할 까?
정말 간단하게
<Box drag .../> 즉 drag하고 싶은 컴포넌트에 drag만 적어두면 끝..!
정말 간단하다!
그럼 드래그 했을 때, 배경색상이 변경되도록 한번 코드를 짜보자!

backgroundColor: 'rgb(46,204,113)' : 요소가 드래그되는 동안 backgroundColor가 'rgb(46,204,113)'으로 변경된다. --> 드래그가 시작되면 요소의 배경색이 바로 rgb(46,204,113)으로 변경되기 시작하고, 이 변화가 완전히 완료되기까지 10초가 걸리는 것을 의미한다.

10초 동안 서서히 색깔이 rgb(46,204,113) 색으로 변경되는 것을 확인할 수 있다!
근데, 지금은 이곳 저곳 다 드래그가 되서 쭉 드래그가 되서 화면 밖으로 빠지기도 한다. 이를 제한을 줘보도록 하겠다!

이 두가지 기능을 넣어보겠다

여기서 뜬금없이 useRef를 사용한다고 생각할 수도 있다 (나도 그랬으니까!)
useRef가 BiggerBox 컴포넌트의 DOM 요소를 참조하여 Box의 드래그 제한을 설정하는 데 사용된다.
구체적으로, dragConstraints 속성에 useRef로 참조한 DOM 요소를 전달함으로써 드래그 가능한 범위를 설정한것이다.
useRef 훅 사용 방식
const biggerBoxRef = useRef<HTMLDivElement>(null);<BiggerBox ref={biggerBoxRef}><Box drag dragConstraints={biggerBoxRef} />useRef를 사용하여 BiggerBox의 DOM 요소를 참조하고, 이 참조를 dragConstraints에 전달하여 드래그 가능한 범위를 BiggerBox로 제한한 것
dragConstraints={biggerBoxRef}: drag 상태의 요소가 특정 영역 내에서만 드래그되도록 제한
dragSnapToOrigin: 드래그가 끝난 후 요소를 원래 위치로 되돌린다.
MotionValues는 애니메이션 값의 상태(state)와 속도(velocity)를 추적한다.
모든 모션 컴포넌트는 내부적으로 MotionValues를 사용하여 애니메이션 값의 상태와 속도를 추적하게 되는데, 일반적으로 이들은 자동으로 생성된다.
(MotionValue는 React State가 아니기 때문에 Motion Value값이 바뀌어도 리랜더링이 일어나지 않는다.)
<Wrapper>
<Box drag="x" dragSnapToOrigin />
</Wrapper>
이렇게 속성을 제공해주어 <Box/> 컴포넌트가 x축으로만 이동이 가능하고, 드래그를 넣으면 원래상태로 돌아올 수 있도록 속성을 제공해주었다.
그럼 이제 x의 값을 한번 가져와볼까 ?

useMotionValue 훅을 사용하여 x라는 MotionValue를 생성하고 있다.
useMotionValue(0) 은 초기값으로 0을 가지는 MotionValue 객체를 생성한 것이다.
이를 사용하기 위해서는
<Box> 컴포넌트의 style 속성을 설정할 때, style={{ x }}와 같이 객체 형태로 MotionValue를 직접 참조할 수 있다.
MotionValue를 사용할 때
style={{ x }}와 같이 직접적으로 사용하는 것은 주로 Framer Motion에서 애니메이션을 구현할 때의 방식이다.
이렇게 콘솔을 한번 찍어볼까 ?
하면 콘솔에 아무것도 찍히지 않는 것을 확인할 수 있다.
Framer Motion의 useMotionValue 훅을 사용하여 생성된 x는 일반적인 JavaScript 변수나 객체가 아닌, Framer Motion이 제공하는 MotionValue 객체이다.
이 객체는 내부적으로 애니메이션 값을 관리하고, 변경할 때마다 UI를 업데이트하게 된다. 따라서 console.log(x)로는 MotionValue 객체의 상태를 직접 확인할 수 없다.
그럼 어떻게 x의 값을 얻어올 수 있을 까?
useEffect(() => {
x.onChange(() => console.log(x.get()));
});
x.onChange : x 객체의 onChange 메서드를 호출하여, MotionValue의 값이 변경될 때마다 호출되는 콜백 함수를 등록한다.
즉, x의 값이 변경 시에 console.log(x.get())를 호출하여 MotionValue의 현재 값을 콘솔에 출력할 수 있게 되었다!

엄청 잘~~~ 찍히는 것을 확인할 수 있다.
그럼, 버튼을 누르면 내가 설정한 x값으로 이동시킬 수 있지 않을까?
<button onClick={() => x.set(500)}>나를 눌러봐!</button>
버튼에 onClick 이벤트를 주어서, 클릭했을 때, x의 값을 set-> 500으로 설정한다!

y도 한번 적용시켜볼까 ?
이렇게 하면 이제 버튼을 누를 때, x -> 300, y -> 300 좌표로 이동하는 것을 볼 수 있다!
useTransform은 Framer Motion 에서 제공하는 훅(Hook) 중 하나로, MotionValue의 값을 변환하는 데 사용된다.

useTransform의 구조
const scale = useTransform(x, [-800, 0, 800], [2, 1, 0.1]);
useTransform(x, [-800, 0, 800], [2, 1, 0.1])에서:
(여기서 scale이라는 이름은 아무렇게나 지어도 된다.)
<Box style={{ x, scale: scale }} drag="x" dragSnapToOrigin />
useTransform을 통해 x의 값에 따라 변환된 크기 값을 가진다.