---
title: "framer-motion과 Flubber를 이용해 우아한 svg 애니메이션 만들기"
description: "motionValue와 useTransition, flubber를 이용해 자연스러운 svg 애니메이션을 구현해보자!"
date: 2023-09-10
tags: [react, javascript, css]
sections: [technical]
draft: false
---

![svg animation](https://velog.velcdn.com/images/ksr20612/post/bab74193-68de-4fe6-bb68-362ece4c29b9/image.gif)

[ Rockie Talkie ] 를 비롯한 React 기반의 개인 프로젝트에서 [🔗 framer-motion](https://www.framer.com/motion/)을 사용하고 있습니다. 간결하지만 강력하고 확실한 기능을 보장하기 때문입니다. Framer-motion이 제공하는 독특한 기능 중 하나로 ```motionValue``` 와 ```useTransition``` 의 조합을 꼽을 수 있는데요, **선언적**인 형태로 애니메이션 조작할 수 있다는 점에서 주목할 만합니다(애니메이션이라는 특성상 완전히 선언적일 수는 없지만요). 이 글에서는 framer의 motionValue와 useTransform, flubber의 조합을 통해 구현하기 어려운 svg 사이의 애니메이션을 ***우아하게*** 구현해보겠습니다. 

## 🤔 motionValue와 useTransform

framer-motion의 모든 ```motion``` 컴포넌트는 내부적으로 ```motionValue``` 라는 값을 가지고 있습니다. ```motionValue```는 애니메이션을 실행하는 데 사용되는 속도나 상태값의 표현이며, 애니메이션이 진행됨에 따라 변경되는 값(변수)입니다. framer-motion에서는 ```useMotionValue```를 제공해 이 값을 제어할 수 있는 훅을 제공해주는데요, 이렇게 활용할 수 있습니다.

```javascript
import { motion, useMotionValue } from "framer-motion";

const Component = () => {
 const x = useMotionValue(0);
 return <motion.div style={{ x }} />
}

export default Component;
```

이로써 우리는 ```useMotionValue```의 초기값으로 0을 전달하고, 애니메이션에 의해 발생되는 x의 변화를 추적할 수 있습니다.

framer-motion은 ```useTransform```이라는 훅 역시 제공하고 있는데요, useTransform을 통해 한```motionValue```의 변경에 따른 연쇄적인 효과를 생성할 수 있습니다. 이는 애니메이션에 대한 매우 강력한 제어권과 유연성을 부여할 수 있습니다. 형태의 변화를 ***동시적***으로 발생시킬 수 있기 때문입니다.

 ***Recoil*** 의 Selectors의 개념에 익숙한 분들은 useTransform의 강력함에 대해 공감할 수 있으실 겁니다. 하나의 데이터로부터 파생된 데이터는 원본 데이터의 변경에 의존하기 때문에 매우 강력한 기능이라고 할 수 있습니다.

> Derived state is a powerful concept because it lets us build dynamic data that depends on other data. - Recoil Selector에서 발췌

framer-motion의 ```useTransform```은 특히 ***특정 범위 내***에서 결정되는 의존적인 ```motionValue```를 만든다는 점에서 더 강력합니다.

```javascript
const x = useMotionValue(0);
const opacity = useTransform(
 x,
 // Map x from these values:
 [0, 100],
 // Into these values:
 [1, 0]
)
```

```x```라는 ```motionValue```값이 0에서 100에서 변경될 때, ```useTransform```으로 선언한 ```opacity```의 값은 1에서 0으로 변경됩니다. 즉, 위치와 투명도가 서로 의존적인 관계를 맺고 동시에 변경되는 것입니다.


## 🌈 svg 애니메이션 구현하기

flubber를 살짝 얹으면 svg 사이의 애니메이션을 손쉽게 구현할 수 있습니다. [🔗 flubber](https://www.npmjs.com/package/flubber) 는 2d 형태로 구현된 문양 사이를 자연스럽게 ***interpolate*** 해주는 api를 제공합니다. flubber의 ```flubber.interpolate()```를 이용하면 쉽게 svg 애니메이션을 구현할 수 있습니다.

> ***interpolation***: 우리말로 옮기면 '보간'. 필자에게도 낯선 단어이긴 하지만, 위에서 살펴본 useTransform의 사례가 0~100의 값을 1,0으로 interpolate해준다고 이해하면 될 듯합니다.

!codesandbox[flubber-with-framer-motion-cy5l8z?fontsize=14&hidenavigation=1&theme=dark]

```motionValue```와 ```useTransition```, 그리고 커스텀 훅인 ```useFlubber```을 통해 어떻게 애니메이션을 구현했는지 살펴보겠습니다.

### 1. useMotionValue와 useTransform으로 motionValue 생성하기

```javascript
// App.tsx
const paths = [twitter, git, facebook, pinterest, twitter]; // svg strings
const colors = ["#16d1f2", "#000000", "#3b5998", "#CD201F", "#16d1f2"];

const App = () => {
 const [pathIndex, setPathIndex] = useState(0);
 const progress = useMotionValue(pathIndex);
 const fill = useTransform(progress, paths.map(getIndex), colors);
 const path = useFlubber(progress, paths);

 useEffect(() => {
 const animation = animate(progress, pathIndex, {
	 onComplete: () => {
 ...
 setPathIndex(pathIndex + 1);
 ...
 }
 });
 ...
 }, [pathIndex]);

 return (
	...
 <motion.path fill={fill} d={path} />
	...
 );
};
```

progress와 fill, path는 모두 ```motionValue```입니다. 이때 ```progress```와 ```fill``` 값은 서로 ***의존적***입니다. ```progress```의 값이 0에서 4(```paths```의 원소 개수와 동일합니다. 제작한 getIndex 함수는 [0, 1, 2, 3, 4]를 리턴)로 변할 때 ```fill```의 값은 colors의 각 원소들(hex 색상값)로 변경됩니다. ```useTransform```을 통해 새로운 범위의 값을 매핑해주었기 때문이죠. ```fill```은 motion.path 컴포넌트의 ```fill``` 속성에 매핑되어, DOM 위에서 표현되는 DOM 엘리먼트의 색상을 담당하게 됩니다.

### 2. flubber 살짝 얹기

그렇다면 모양을 결정하는 ```path```는 어떤 값을 가지고 있을까요? 제작한 ```useFlubber``` 훅을 살펴보도록 하겠습니다.

```javascript
// use-flubber.tsx
const useFlubber = (progress: MotionValue, paths: string[]) => {
 return useTransform(progress, paths.map(getIndex), paths, {
 mixer: (a, b) => interpolate(a, b, { maxSegmentLength: 0.1 }),
 });
}
```

```useFlubber```는 ```useTransform```을 내부적으로 감싸고 있는 커스텀 훅입니다. ```paths.map(getIndex)```로 생성된 [0, 1, 2, 3, 4]의 값(```progress```가 가질 수 있는 값)은 다섯 가지의 모양(svg)를 원소로 하는 ```paths```의 값과 ***interpolated***됩니다. 

핵심은 옵션으로 전달하는 ***mixer*** 입니다. 문서를 확인해보면, ```mixer``` 옵션으로는 다음과 같은 함수를 전달할 수 있습니다.

```javascript
mixer: (from: T, to: T) => (p: number) => any
```

```mixer```로 전달되는 함수의 인자 ```a```와 ```b```는 연속적으로 변경되는 데이터 결과값의 쌍을 의미합니다. 여기서는 ```paths```의 값들이 되겠네요. 즉, {a, b}는 {twitter, git}, {git, facebook}, {facebook, pinterest}, {pinterest, twitter}의 다섯 가지 쌍을 의미합니다.

```interpolate```는 자연스럽게 ```(p: number) => any```를 리턴한다고 생각할 수 있겠는데요, 예시로 이해해보겠습니다.

```javascript
const interpolator = flubber.interpolate(triangle, octagon);

interpolator(0); // returns an SVG triangle path string
interpolator(0.5); // returns something halfway between the triangle and the octagon
interpolator(1); // returns an SVG octagon path string
```

```flubber.interpolate```는 일종의 ***커링 함수*** 입니다. 0에서 1 사이의 인자를 전달받으면 두 개 모양(svg) 사이의 ```interpolated```된 문양을 리턴합니다. 즉, ```(p: number) => string``` 함수를 리턴하는 셈입니다. 위 사례에서는 triangle과 octagon 사이의 모양이 분절적인 형태로 각 0 ~ 1에 할당되겠네요.

이 값, 많이 익숙하지 않나요? 맞습니다, ```mixer```에 할당하는 함수 연쇄 일부와 동일합니다. ```flubber.interpolate```를 사용해 두 문양 사이의 값을 결정할 수 있겠네요. ```maxSegmentLength```를 통해 ***얼마나 촘촘하게*** 문양을 분절할 것인지를 전달할 수 있습니다. 이 값이 작으면 더 자연스럽게 모양이 변경될 수 있습니다(더 촘촘하게 잘리니까요).

```javascript
mixer: (a, b) => interpolate(a, b, { maxSegmentLength: 0.1 })
```

## 🙌 마치며

```useMotionValue```, ```useTransform```, ```flubber```을 사용하여 svg 사이의 애니메이션을 구현해보았습니다. 서로 의존적인 데이터를 만들어보기도 하고, ```interpolation```을 이용해보기도 했습니다.

```styled-components```에서도 ***Interpolated styles*** 원칙을 통해 컴포넌트(유니크한 클래스명)를 생성하는데요, 나중에 styled-components의 동작 원리를 정리하면서 ***Interpolation***에 대해서 더 알아보도록 하겠습니다.
