👷♂️ Framer Motion에 친숙하지 않다면 이 포스트를 읽어주세요
본 글에서는 Framer Motion
으로 아래와 같은 Tab
컴포넌트를 구현하기위해 알아야할 개념을 다룬다.
Layout
props를 true
로 설정하면 컴포넌트의 layout
이 변할 때 그 모습을 부드럽게 이어준다. layout
이 변하는 경우를 다음과 같다.
width
나 position
이 변했을 때flex
혹은 grid
로 변할 때)layout props를 이용하면 다음과 같은 컴포넌트를 만들 수 있다. 우선 체험해보고 자세히 알아보자.
export default function App() {
const [isOn, setIsOn] = useState(false);
const toggleSwitch = () => setIsOn(!isOn);
return (
<div className="switch" data-isOn={isOn} onClick={toggleSwitch}>
<motion.div className="handle" transition={spring} />
</div>
);
}
const spring = {
type: "spring",
stiffness: 700,
damping: 30,
};
예시로 살펴본 버튼은 이런 동작 원리는 다음과 같다. 사용자가 토글버튼을 클릭하면 data-isOn
의 값이 변하면서 css의 justify-content
가 부드럽게 변한다. (css파일에서 data-isOn에 따라 스타일을 다르게 하는 코드가 있다.)
여기서 핵심은 ”부드럽게” 변하는 것이다. 본래 justify-content
속성은 transition과 animation에서 사용할 수 없다! transition이 가능한 속성 목록 에서 transition 과 animation에서 사용할 수 있는 속성을 볼 수 있다. 하지만 justify-content
는 찾을 수 없다.
토글 버튼에 layout
props를 삭제하고 css에서 transition을 주었다. 이해를 돕기위해 클릭시에 background-color
를 변경시켰다. 이제 다시 클릭해보자.
배경 색깔에는 transition이 적용되지만 justify-content
에서는 적용되지 않았다.
🚧 Framer-Motion에 따르면 이런 레이아웃의 변화를
transform
속성을 이용해서 처리했다고 한다. 본래의 속성을 이용해서 변화시키지 않기때문에 자식 컴포넌트들을 왜곡시킬 수 있다. 왜곡이 일어나는 경우에는 첫 번째 자식컴포넌트에도 layout props를true
로 설정해주는 것을 추천한다.
{items.map(item => (
<motion.li layout>
{item.name}
{item.isSelected && <motion.div layoutId="underline" />}
{//item.isSelected가 true라면 <motion.div layoutId="underline"/> 컴포넌트가 랜더링된다.}
</motion.li>
))}
다음은 우리가 만들 목표 컴포넌트의 일부이다. 아이템을 클릭하면 selected
가 true
가 되는 Tab 아이템이 있다. true
일 때는 밑줄 모양의 divElement
가 랜더링된다.
다른 아이템을 선택하면 그 아이템에 밑줄이 랜더링된다. 여기서 layoutId
를 주목하자. 같은 layoutId
를 가진 컴포넌트가 언마운트 되는 동시에 같은 layoutId
를 가지는 컴포넌트가 새롭게 생긴다면 Framer Motion
은 컴포넌트가 사라진 것처럼 보여주지 않고 애니메이션으로 부드럽게 이어준다. layoutId
가 없거나 같지 않다면 뚝뚝 끊기는 모습을 보인다.
layoutId
기능을 키고 끌 수 있게 변형시켰다. 토글을 눌러보면서 차이를 확인하자. 기능을 끄고 탭 아이템들을 클릭하면 밑줄이 뚝뚝 끊기는 모습이 보인다. 기능을 키면 부드럽게 움직이는 모습을 볼 수 있다.
바로 위 예시에서 탭 아이템들을 클릭해보자. 이번에는 클릭하면 변하는 음식들을 유심히 보자. 음식이 변할 때 언마운트되는 음식이 살짝 올라가는 애니메이션을 볼 수 있다. Framer Motion의 AnimatePresence
컴포넌트는 컴포넌트가 언마운트될 때 exit animation
을 가능하게 해준다.
exit animation
은 motion
컴포넌트의 exit
props에 애니메이션 상태를 전달하는 방식으로 결정된다. 이는 animate
, initial
과 같은 방식이다. 다음은 이미지 슬라이더를 만들 때 사용할 수 있는 코드다.
export const Slideshow = ({ image }) => (
<AnimatePresence>
<motion.img
key={image.src}
src={image.src}
initial={{ x: 300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -300, opacity: 0 }}
/>
</AnimatePresence>
)
❗자식 컴포넌트들은 유일한
key
를 가져야만AnimatePresence
가 제대로 동작한다. React는 리액트는 key가 다른 컴포넌트를 아예 다른 컴포넌트로 생각하고 동작한다. 이런 이유 때문에 다음에 올 컴포넌트가 마운트되는 동시에 이전 컴포넌트의exit animation
를 실행시키게 해준다.
이미지 슬라이더를 보면 사진이 사라지기 전에 다음 사진이 미리 마운트되는 모습을 볼 수 있다.
변하는 음식 컴포넌트는 이런 방식으로 구현되었다.
<main style={{ position: "relative" }}>
<ToggleBtn isLayout={isLayout} setIsLayout={setIsLayout} />
<AnimatePresence exitBeforeEnter>
{//true 설정시 exitBeforeEnter는 새로운 컴포넌트가 나타나직 전에 이전 컴포넌트가 사라진다.}
<motion.div
key={selectedTab ? selectedTab.label : "empty"}
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: 20 }}
exit={{ opacity: 0, y: -20 }} //올라가면서 흐려지는 애니메이션
transition={{ duration: 0.15 }}
>
{selectedTab ? selectedTab.icon : "😋"}
</motion.div>
</AnimatePresence>
</main>
layout
, AnimatePresence
는 vanila로 구현하기엔 다소 까다로운 UI를 쉽게 구현할 수 있게 해준다. 이 둘을 가지고 만들 수 있는 UI는 정말 다양하다. 쓸데없이 멋지기만 한 애니메이션이 아니라 사소하지만 사용자 경험을 풍부하게 하는 애니메이션을 구현할 수 있다.
개인적으로 페이지 전환 애니메이션을 좋아하는데 이를AnimatePresence
로 구현할 수 있다. 다음 포스트에서는 이를 React
, Next
에서 어떻게 구현할 수 있는지 구체적인 코드를 가지고 이야기해보겠다.
🎈 참고 자료
도움이 많이 됐습니당!