리엑트앱에서 애니메이션을 잘 활용하게 된다면 아름다운 앱을 만드는 것이 더욱 편해진다.
이런 애니메이션을 편하게 하기위한 대표적인 패키지가 framer-motion이라는 패키지이다.
설치는 다음과 같이 진행을 하면 된다.
npm install framer-motion
framer motion을 사용하기 위해서는 기존 랜더링이 되던 html 컴포넌트, 예를 들어 div를 랜더링을 한다면 해당 컴포넌트를 motion.div로 변경을 해주면 된다.
만약에 styled-componenets를 사용해 커스텀화 된 html 컴포넌트를 랜더링하고 있다면 styled-component에 다음과 같이 motion을 추가해주면 된다.
const Box = styled(motion.div)`
width: 200px;
height: 200px;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;
그 후 해당 컴포넌트에 아래와 같이 animation을 추가해주면 된다.
function App() {
return (
<Wrapper>
<Box transition={{duration:3}} animate={{borderRadius: "100px"}}/>
</Wrapper>
);
}
해당 앱을 실행 시켜본다면 3초 동안 네모난 상자가 borderRadius 100px인 원으로 변하는 것을 볼 수 있다.
위의 예시처럼 Html 컴포넌트 옆에 애니메이션을 전부 작성한다면 코드가 더러워질 수 있다.
이를 위해서 사용할 수 있는 것이 variants다.
먼저 myVariants라는 변수를 다음과 같이 만들어 준다.
const myVariant = {
start: { scale: 0 },
end: { scale: 1, rotateZ: 360, transition: { type: "spring", delay: 0.5 } },
};
그 후 Box 컴포넌트에 다음과 같이 적용을 해줄 수 있다.
<Box variants={myVariant} initial="start" animate="end"/>
적용할 variants를 지정해주면 해당 variant 컴포넌트를 순회하면서 이름에 맞는 스타일을 적용해준다.
부모 Variants의 속성을 자식 컴포넌트에 상속시키는 것 역시 가능하다.
const boxVariants = {
start:{
opacity:0,
scale:0.5,
},
end:{
opacity: 1,
scale: 1,
transition:{
type:"spring",
duration:0.5,
bounce:0.5,
delayChildren:0.5,
staggerChildren:0.2,
}
},
};
const circleVariants = {
start:{
opacity:0,
y:10,
},
end:{
opacity:1,
y:0,
},
};
function App() {
return (
<Wrapper>
<Box variants={boxVariants} initial="start" animate="end">
<Circle variants={circleVariants}/>
<Circle variants={circleVariants}/>
<Circle variants={circleVariants}/>
<Circle variants={circleVariants}/>
</Box>
</Wrapper>
);
}
Box라는 부모 컴포넌트 밑에 Circle이라는 자식 컴포넌트 4개를 랜더링한다고 했을때, 자식 컴포넌트에서도 부모 컴포넌트의 start와 end 값을 별도로 지정하지 않아도 사용할 수 있다.
다만 당연하겠지만, variants내의 변수명은 부모와 동일하게 적용해야 자동적으로 적용이 가능하다.
또한 부모 variants에서 자식 컴포넌트들을 조종할 수 있는데, delayChildren을 통해 자식 컴포넌트가 부모의 애니메이션이 끝난 얼마 후 실행하게 할 수 있고, staggerChildren을 통해서 자식 컴포넌트 간 애니메이션을 얼마나 딜레이할지도 지정할 수 있다.
특정 컴포넌트를 드래그하고 싶은 경우가 있을탠데 이를 구현하는 것은 정말 간단하다.
<Box drag variants={boxVariants} whileHover="hover" whileTap="click"/>
해당 컴포넌트에 drag을 추가해주면 해당 box가 드래그 가능한 것을 볼 수 있다.
컴포넌트가 없어지면서 애니메이션을 추가하고 싶을때 Animate Presence를 사용하면 된다.
예를 들어 버튼을 눌렀을때 box가 나오고 다시 버튼을 누르면 box가 사라지는 애니메이션을 구현하려고 한다.
const boxVariant = {
initial: {
opacity:0,
scale:0,
},
visible: {
opacity:1,
scale:1,
rotateZ: 360,
},
leaving: {
opacity:0,
y:50,
},
}
function App() {
const[showing, setShowing] = useState(false);
const toggleShowing = () => setShowing((prev) => !prev);
return (
<Wrapper >
<button onClick={toggleShowing}>Click</button>
<AnimatePresence>{showing? <Box variants ={boxVariant} initial="initial" animate="visible" exit="leaving"/>: null}</AnimatePresence>
</Wrapper>
);
}
UseState을 사용해서 box의 보이는것의 상태를 확인하고 toggleShowing 함수를 통해 해당 상태값을 변경해준다.
그리고 해당 랜더링하는 부분을 AnimatePresence를 통해서 감싸주면 해당 객체가 없어질때도 정상적으로 애니메이션이 적용이 되는 것을 확인할 수 있다.
Framer-motion에서 layout이라는 것을 활용해서 컴포넌트간 애니메이션을 쉽게 구현할 수 있다.
const [clicked, setClicked] = useState(false);
const toggleClicked = () => setClicked((prev) => !prev);
return (
<Wrapper onClick={toggleClicked}>
<Box>
{!clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 50 }} />
) : null}
</Box>
<Box>
{clicked ? (
<Circle layoutId="circle" style={{ borderRadius: 0, scale: 2 }} />
) : null}
</Box>
</Wrapper>
);
useState을 통해 클릭이 됐는지 확인하는 상태를 만들어주고,
하나의 박스에는 클릭이 됐을때 원이 랜더링 되게, 하나의 박스는 클릭이 되지 않았을때 원이 랜더링 되지 않게 컴포넌트를 만들었다.
이때 박스 안에 랜더링 되는 원 컴포넌트 안에 layoutId를 동일하게 설정을 했을때 원이 박스 간에 랜더링이 되는 것을 확인할 수 있다.