react 포트폴리오를 개발하던 중 framer-motion과 react로 Typography, 텍스트에 입력하는듯한 애니메이션을 부여하고 싶어졌다.
첫번째 시도는 단순히 제공받은 텍스트 문자열을 특정 주기로 자르도록 setTimeout 체인을 만드는 것이였다.
const ProgressiveTypograpy: React.FC<{
label: string,
speed?: number
} & Omit<TypographyProps, 'children'>> = ({
label,
speed = 500,
...props
}) => {
const [prog, setProg] = useState(0);
useEffect(() => {
for(let i = 0; i < label.length; i++) {
setTimeout(() => setProg(prev => prev+1), speed);
}
}, []);
return <Typography {...props}>{label.slice(0, prog)}</Typography>
}
하지만 아래와 같이 문자들이 연속적으로 등장하지 않아 실패했다.
setState의 비동기적 상태관리가 내 생각보다 더 lazy한 것이였다. 재귀적 방법 또한 시도해봤으나 근본적으로 setState에 대한 문제라 역시 실패했다. 500ms가 이러하니 더 빨리해도 소용이 없고 좋지 않은 방법이라 판단해 포기했다.
잠시 고민을 하고 구글링을 하려던 참에, 비생산적이겠지만 차라리 문자열을 문자 엘리먼트로 쪼개어 framer-motion의 animate속성의 keyframe 특징을 이용하여 단계별 등장을 유도하면 어떨까 라는 생각이 들어 바로 시도해보았다.
//...윗부분 생략
}) => {
const variants = {
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i * 0.3,
},
}),
hidden: { opacity: 0 },
}
//mui의 Typography가 내 생각보다 무겁지 않기를 빌며...
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
{label.split('').map((char, i) =>
<motion.div
key={i}
custom={i}
initial='hidden'
animate="visible"
variants={variants}
whileInView='visible'
>
<Typography {...props}> {char} </Typography>
</motion.div>)}
</div>
);
}
역시 framer-motion는 최고다.
속도를 빠르게 해서 각 철자간 애니메이션 간극이 사라질 수 있나 싶었는데 잘도 사라진다.
import { Typography, type TypographyProps } from '@mui/material';
import { motion } from 'framer-motion';
import React from 'react';
const ProgressiveTypography: React.FC<{
label: string,
speed?: number
} & TypographyProps> = ({
label,
speed = 0.1,
...props
}) => {
const variants = {
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i * speed,
},
}),
hidden: { opacity: 0 },
}
//mui의 Typography가 내 생각보다 무겁지 않기를 빌며...
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
{label.split('').map((char, i) =>
<motion.div
key={i}
custom={i}
initial='hidden'
animate="visible"
variants={variants}
whileInView='visible'
>
<Typography {...props}>{char}</Typography>
</motion.div>
)}
</div>
);
}
export default ProgressiveTypography;
아무튼 완성!