토스 앱을 사용하다 보면, 텍스트가 한 번에 나타나지 않고 글자 하나하나가 마치 도미노처럼 순차적으로 떠오르는 애니메이션을 볼 수 있습니다. 단순히 문장이 통째로 등장하는 것을 넘어, 글자 단위로 미세한 시차를 두고 나타나는 디테일한 UX에 큰 매력을 느끼게 되어 직접 구현해 보게 되었습니다.
이번 포스팅에서는 텍스트를 한 글자씩 분리하여 순차적으로 애니메이션을 적용하는 DominoText 인터렉션을 플러터에서 구현하는 과정을 공유하려 합니다. 문자열을 분리해 개별 AnimationController를 할당하는 방법부터, 약간의 무작위성(Jitter)을 부여해 기계적이지 않고 자연스러운 타이밍을 만드는 디테일한 구현 과정까지 설명드리겠습니다.
💡 본 포스팅에서 소개하는 DominoText 위젯을 쉽게 적용할 수 있도록 패키지를 개발하였습니다. 향후 더 많은 멋진 인터렉션을 해당 패키지에 추가할 예정이니 많은 관심 부탁드립니다!
package :
cool_animation_flutter
github : https://github.com/yundal8755/cool_animations
저는 이 인터렉션을 글자들이 도미노처럼 차례대로 반응한다는 특징에 착안하여 DominoText 위젯이라고 정의했습니다.
가장 큰 특징은 텍스트가 통째로 움직이는 것이 아니라, “1분 만에 대출 금리·한도 확인할 수 있다”라는 문장이 주어졌을 때 1, 분, ' ', 만, 에 … 이런식으로 각 글자가 아주 짧은 시차를 두고 아래에서 위로 떠오르며 페이드 인 된다는 점입니다. 이를 통해 텍스트 자체에 생동감을 부여할 수 있습니다.
이전 포스팅이었던 SlideUpFadeIn에서는 하나의 위젯을 통째로 제어했기 때문에 단일 AnimationController로 충분했습니다. 하지만 글자마다 애니메이션의 시작 시점이 달라야 하는 도미노 텍스트의 특성상, 이번에는 텍스트 길이만큼의 다중 컨트롤러를 리스트로 관리해야 합니다.
🤔 왜 컨트롤러를 여러 개 사용할까요?
하나의 컨트롤러 안에서 Interval 등을 사용하여 쪼갤 수도 있지만, 텍스트의 길이가 가변적이고 긴 문장일 경우 0.0~1.0 사이의 값을 수십 개로 쪼개어 연산하는 것은 관리와 확장이 까다로울 수 있습니다. 따라서 각 글자마다 독립적인 생명주기를 가지는 AnimationController를 매핑해주는 직관적인 방식을 채택했습니다.
void _initControllers() {
// 줄바꿈 문자를 제외한 순수 글자 수를 계산합니다.
final charCount = widget.text.replaceAll('\n', '').length;
for (int i = 0; i < charCount; i++) {
final controller = AnimationController(
vsync: this,
duration: widget.duration,
);
final curved = CurvedAnimation(
parent: controller,
curve: widget.curve,
);
_controllers.add(controller);
_curvedAnimations.add(curved);
}
}
이렇게 하면 _controllers 리스트 안에 각 글자의 애니메이션을 책임질 컨트롤러들이 준비됩니다. 화면에 그려질 때는 Wrap 위젯 안에서 각 글자를 쪼개어 AnimatedBuilder로 감싸고, 이전 포스팅에서 다루었던 lerpDouble을 활용해 투명도와 Y축 위치를 제어해주면 됩니다.
컨트롤러들을 만들었다면, 이제 이 컨트롤러들을 언제 실행시킬 것인지가 이 위젯의 핵심 퀄리티를 결정합니다. 단순히 for문을 돌리며 일정한 staggerDelay만 주면 너무 기계적인 느낌이 들 수 있습니다.
그래서 저는 jitterMs라는 변수를 추가했습니다. 여기서는 각 글자가 등장하는 딜레이 시간에 약간의 랜덤한 오차를 부여하여, 훨씬 더 리듬감 있는 타이핑/도미노 효과를 만들어냅니다.
Future<void> _startAnimation() async {
if (widget.delay > Duration.zero) {
await Future<void>.delayed(widget.delay);
if (!mounted) return;
}
final random = Random();
for (int i = 0; i < _controllers.length; i++) {
if (!mounted) return;
if (i > 0) {
// Jitter 값에 따라 딜레이 시간에 약간의 랜덤 오차를 줍니다.
final jitter = widget.jitterMs > 0
? random.nextInt(widget.jitterMs * 2 + 1) - widget.jitterMs
: 0;
final effectiveDelay = Duration(
milliseconds: (widget.staggerDelay.inMilliseconds + jitter).clamp(0, 9999),
);
// 계산된 딜레이만큼 기다린 후 다음 글자 애니메이션을 시작합니다.
await Future<void>.delayed(effectiveDelay);
if (!mounted) return;
}
// 개별 컨트롤러 실행!
_controllers[i].forward();
}
}
이 로직을 통해 글자들이 차례대로 나타나게되며, 앞서 적용한 lerpDouble 보간 함수에 의해 각 글자가 스르륵 제자리를 찾아가게 됩니다.
이번 글에서는 토스나 감각적인 UI를 가진 서비스에서 자주 활용하는 타이포그래피 도미노 애니메이션을 구현해 보았습니다.
단일 애니메이션 위젯에서 한 단계 더 나아가, 다중 AnimationController를 관리하고 Jitter 개념을 도입하여 애니메이션에 섬세한 리듬감을 불어넣는 방법을 알아보았습니다.
앱의 메인 온보딩 화면이나 특정 혜택을 강조해야 하는 타이틀에 이 DominoText 위젯을 적용해 보세요. 텍스트 하나만으로도 앱의 첫인상이 훨씬 고급스러워질 것입니다. 해당 기능이 포함된 cool_animation_flutter 패키지에도 많은 관심 부탁드립니다.
긴 글 읽어주셔서 감사합니다!
원하시는 추가적인 인터렉션 분석이나 코딩 도움이 필요하시다면 언제든 말씀해주세요!
항상 잘 보고 있습니다. 애니메이션에 Jitter로 디테일 살리신 부분 정말 감탄하고 갑니다! 오늘도 좋은 글 감사합니다.