사용자 경험(UX)의 시대에, 정적인 화면은 더 이상 사용자의 시선을 사로잡기 어렵습니다. 부드럽고 직관적인 애니메이션은 앱에 생명력을 불어넣고, 사용자에게 시각적 피드백을 제공하며, 앱의 전반적인 품질을 한 차원 높여주는 핵심 요소입니다. Flutter는 강력하고 유연한 애니메이션 시스템을 제공하여 개발자가 아름다운 UI를 쉽게 만들 수 있도록 지원합니다. 하지만 많은 개발자들이 어디서부터 시작해야 할지, 어떤 애니메이션 기법을 사용해야 할지 막막해합니다.
Flutter 애니메이션의 세계는 크게 두 가지 갈래로 나뉩니다: 암시적(Implicit) 애니메이션과 명시적(Explicit) 애니메이션. 이 두 가지 접근법은 각각의 장단점과 사용 사례가 명확하며, 이 둘의 차이를 이해하는 것이 Flutter 애니메이션을 효과적으로 활용하는 첫걸음입니다. 이 글에서는 암시적 애니메이션의 간편함부터 명시적 애니메이션의 정교한 제어까지, 두 가지 방법을 심층적으로 파헤치고 실제 코드 예제를 통해 완벽하게 이해할 수 있도록 돕겠습니다.
1부: 가장 쉬운 시작, 암시적 애니메이션 (Implicit Animations)
암시적 애니메이션은 '알아서 해주는' 애니메이션입니다. 개발자는 애니메이션의 시작과 끝 상태만 정의하면, Flutter 프레임워크가 그 사이의 전환 과정을 자동으로 부드럽게 처리해 줍니다. 애니메이션의 진행 상태를 직접 제어하기 위한 AnimationController
같은 복잡한 객체를 만들 필요가 없습니다. 그래서 '암시적'이라는 이름이 붙었습니다.
언제 암시적 애니메이션을 사용해야 할까요?
- 위젯의 속성(크기, 색상, 위치 등)이 변경될 때 간단한 전환 효과를 주고 싶을 때
- 사용자의 특정 행동(예: 버튼 클릭)에 대한 일회성 피드백이 필요할 때
- 복잡한 제어 없이, 최소한의 코드로 빠르게 애니메이션을 구현하고 싶을 때
핵심 위젯: AnimatedContainer
암시적 애니메이션의 가장 대표적인 예는 AnimatedContainer
입니다. 이 위젯은 일반 Container
와 거의 동일한 속성을 가지지만, duration
과 curve
속성이 추가되어 있습니다. width
, height
, color
, decoration
, padding
등과 같은 속성값이 변경되면, AnimatedContainer
는 지정된 duration
(지속 시간)과 curve
(가속도 곡선)에 따라 이전 상태에서 새로운 상태로 부드럽게 전환됩니다.
예제 1: 탭할 때마다 색상과 크기가 변하는 상자
가장 기본적인 AnimatedContainer
사용 예제입니다. 버튼을 누를 때마다 상자의 크기와 색상이 랜덤하게 바뀌는 애니메이션을 만들어 보겠습니다.
import 'package:flutter/material.dart';
import 'dart:math';
class ImplicitAnimationExample extends StatefulWidget {
const ImplicitAnimationExample({Key? key}) : super(key: key);
@override
_ImplicitAnimationExampleState createState() => _ImplicitAnimationExampleState();
}
class _ImplicitAnimationExampleState extends State {
double _width = 100.0;
double _height = 100.0;
Color _color = Colors.blue;
BorderRadiusGeometry _borderRadius = BorderRadius.circular(8.0);
void _randomize() {
final random = Random();
setState(() {
_width = random.nextDouble() * 200 + 50; // 50에서 250 사이
_height = random.nextDouble() * 200 + 50; // 50에서 250 사이
_color = Color.fromRGBO(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
1,
);
_borderRadius = BorderRadius.circular(random.nextDouble() * 50);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AnimatedContainer 예제'),
),
body: Center(
child: AnimatedContainer(
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
// 애니메이션의 핵심!
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn, // 부드러운 시작과 끝
child: const Center(
child: Text(
'Animate Me!',
style: TextStyle(color: Colors.white),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _randomize,
child: const Icon(Icons.play_arrow),
),
);
}
}
코드 분석:
- 상태 변수 선언:
_width
,_height
,_color
,_borderRadius
는 컨테이너의 현재 상태를 저장합니다. _randomize
메소드: 버튼을 누르면 호출됩니다.Random
객체를 사용하여 새로운 크기, 색상, 테두리 반경 값을 생성합니다.setState
호출: 가장 중요한 부분입니다.setState
내에서 상태 변수들을 업데이트하면 Flutter는 위젯 트리를 다시 빌드(rebuild)합니다.AnimatedContainer
의 마법: 위젯이 다시 빌드될 때,AnimatedContainer
는 자신의 새로운 속성값(_width
,_color
등)이 이전 빌드 때의 값과 다른 것을 감지합니다. 그러면 내부적으로 애니메이션을 트리거하여duration
에 설정된 1초 동안 이전 값에서 새 값으로 부드럽게 변경합니다.curve
속성: 애니메이션의 '느낌'을 결정합니다.Curves.fastOutSlowIn
은 시작은 빨랐다가 끝에서 천천히 마무리되는, 매우 자연스러운 느낌을 줍니다.
다양한 애니메이션 효과: Curves
Curve
는 애니메이션의 시간 대비 값의 변화율을 정의합니다. 단순히 직선적으로 변하는 것(Curves.linear
) 외에도 수십 가지의 미리 정의된 곡선이 있어 애니메이션에 개성을 더할 수 있습니다.
Curves.linear
: 등속 운동. 기계적인 느낌을 줍니다.Curves.easeIn
: 천천히 시작해서 점점 빨라집니다.Curves.easeOut
: 빠르게 시작해서 점점 느려집니다.Curves.easeInOut
: 천천히 시작해서 중간에 빨라졌다가 다시 천천히 끝납니다. 가장 일반적으로 사용됩니다.Curves.bounceOut
: 목표 지점에 도달한 후 몇 번 튕기는 효과. 재미있는 느낌을 줍니다.Curves.elasticOut
: 고무줄처럼 목표 지점을 넘어갔다가 되돌아오는 탄성 효과.
위 예제의 curve: Curves.fastOutSlowIn
부분을 curve: Curves.bounceOut
으로 바꾸고 실행해 보세요. 애니메이션의 느낌이 완전히 달라지는 것을 확인할 수 있습니다.
더 많은 암시적 애니메이션 위젯들
AnimatedContainer
외에도 Flutter는 다양한 상황에 쓸 수 있는 'Animated' 접두사가 붙은 위젯들을 제공합니다. 이들은 모두 같은 원리로 동작합니다: 속성값을 바꾸고 setState
를 호출하면 끝입니다.
AnimatedOpacity
:opacity
값을 변경하여 위젯을 서서히 나타나거나 사라지게 합니다. 로딩 인디케이터를 숨기거나 보여줄 때 유용합니다.AnimatedPositioned
:Stack
위젯 내에서 자식 위젯의 위치(top
,left
,right
,bottom
)를 애니메이션으로 변경합니다.AnimatedPadding
: 위젯의padding
값을 부드럽게 변경합니다.AnimatedAlign
: 부모 위젯 내에서 자식의 정렬(alignment
)을 애니메이션으로 변경합니다.AnimatedDefaultTextStyle
: 자식Text
위젯들의 기본 스타일(fontSize
,color
,fontWeight
등)을 부드럽게 변경합니다.
만능 해결사: TweenAnimationBuilder
만약 애니메이션을 적용하고 싶은 속성에 해당하는 AnimatedFoo
위젯이 없다면 어떻게 할까요? 예를 들어, Transform.rotate
의 angle
값이나 ShaderMask
의 그라데이션을 애니메이션하고 싶을 수 있습니다. 이럴 때 TweenAnimationBuilder
가 활약합니다.
TweenAnimationBuilder
는 특정 타입의 값(예: double
, Color
, Offset
)을 시작(begin) 값에서 끝(end) 값으로 애니메이션합니다. 핵심 속성은 다음과 같습니다.
tween
: 애니메이션할 값의 범위를 정의합니다. (예:Tween
)(begin: 0, end: 1) duration
: 애니메이션 지속 시간입니다.builder
: 애니메이션의 매 프레임마다 호출되는 함수입니다. 현재 애니메이션 값, 그리고 애니메이션의 대상이 될 자식 위젯(선택 사항)을 인자로 받습니다. 이 빌더 함수 안에서 현재 값을 사용하여 위젯을 변형시킵니다.
예제 2: 숫자가 카운트업되는 애니메이션
class CountUpAnimation extends StatefulWidget {
const CountUpAnimation({Key? key}) : super(key: key);
@override
_CountUpAnimationState createState() => _CountUpAnimationState();
}
class _CountUpAnimationState extends State {
double _targetValue = 100.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TweenAnimationBuilder 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TweenAnimationBuilder(
tween: Tween(begin: 0, end: _targetValue),
duration: const Duration(seconds: 2),
builder: (BuildContext context, double value, Widget? child) {
// value는 0에서 _targetValue까지 2초 동안 변합니다.
return Text(
value.toStringAsFixed(1), // 소수점 첫째 자리까지 표시
style: const TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
),
);
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_targetValue = _targetValue == 100.0 ? 200.0 : 100.0;
});
},
child: const Text('목표값 변경'),
)
],
),
),
);
}
}
이 예제에서 버튼을 누르면 _targetValue
가 변경되고, TweenAnimationBuilder
는 새로운 end
값을 감지하여 현재 값에서 새로운 목표값까지 다시 애니메이션을 실행합니다. 이처럼 TweenAnimationBuilder
는 특정 위젯에 종속되지 않고 '값' 자체를 애니메이션하기 때문에 활용도가 매우 높습니다.
암시적 애니메이션 요약
- 장점: 배우기 쉽고, 코드가 간결하며, 빠르게 구현할 수 있습니다.
- 단점: 애니메이션을 중간에 멈추거나, 되감거나, 반복하는 등의 복잡한 제어가 불가능합니다. 오직 시작과 끝 상태 사이의 전환만 가능합니다.
이제 더 강력한 제어 기능을 제공하는 명시적 애니메이션의 세계로 넘어가 보겠습니다.
2부: 완벽한 제어를 원한다면, 명시적 애니메이션 (Explicit Animations)
명시적 애니메이션은 개발자가 애니메이션의 모든 측면을 직접 제어하는 방식입니다. 애니메이션의 생명주기(시작, 정지, 반복, 역재생)를 관리하는 AnimationController
를 사용해야 하므로 '명시적'이라는 이름이 붙었습니다. 초기 설정은 암시적 애니메이션보다 복잡하지만, 그만큼 훨씬 더 정교하고 복잡한 애니메이션을 구현할 수 있습니다.
언제 명시적 애니메이션을 사용해야 할까요?
- 애니메이션을 무한히 반복하고 싶을 때 (예: 로딩 스피너)
- 사용자의 제스처(예: 드래그)에 따라 애니메이션을 제어하고 싶을 때
- 여러 애니메이션을 순차적으로 또는 동시에 실행하는 복합적인 애니메이션(Staggered Animation)을 만들고 싶을 때
- 애니메이션을 중간에 멈추거나, 특정 지점으로 이동하거나, 역재생해야 할 때
명시적 애니메이션의 핵심 구성 요소
명시적 애니메이션을 이해하려면 다음 네 가지 핵심 요소를 알아야 합니다.
Ticker
와TickerProvider
:Ticker
는 화면이 새로고침될 때마다(보통 1초에 60번) 콜백을 호출하는 신호기입니다. 애니메이션은 이 신호에 맞춰 값을 변경하여 부드럽게 보입니다.TickerProvider
(주로SingleTickerProviderStateMixin
)는State
클래스에Ticker
를 제공하는 역할을 합니다. 화면이 보이지 않을 때는 Ticker를 비활성화하여 불필요한 배터리 소모를 막아줍니다.AnimationController
: 애니메이션의 '지휘자'입니다. 지정된duration
동안 0.0에서 1.0 사이의 값을 생성합니다..forward()
(재생),.reverse()
(역재생),.repeat()
(반복),.stop()
(정지)과 같은 메소드를 통해 애니메이션을 직접 제어할 수 있습니다.Tween
: '보간'을 의미합니다.AnimationController
가 생성하는 0.0 ~ 1.0 범위의 값을 우리가 실제로 사용하고 싶은 값의 범위(예: 0px ~ 150px, 파란색 ~ 빨간색)로 변환해 줍니다.ColorTween
,SizeTween
,RectTween
등 다양한 종류가 있습니다.AnimatedBuilder
또는...Transition
위젯:Tween
에 의해 변환된 값을 사용하여 실제로 UI를 그리는 역할을 합니다. 애니메이션 값이 변경될 때마다 위젯 트리의 특정 부분만 효율적으로 다시 빌드합니다.
명시적 애니메이션 구현 단계
명시적 애니메이션은 보통 다음 단계를 따릅니다.
StatefulWidget
을 만들고,State
클래스에with SingleTickerProviderStateMixin
을 추가합니다.AnimationController
와Animation
객체를 상태 변수로 선언합니다.initState()
메소드에서AnimationController
와Animation
을 초기화합니다.dispose()
메소드에서AnimationController
를 반드시 해제(dispose)하여 메모리 누수를 방지합니다.build()
메소드에서AnimatedBuilder
나...Transition
위젯을 사용하여 애니메이션 값을 UI에 적용합니다.- 적절한 시점(예: 버튼 클릭)에
_controller.forward()
등을 호출하여 애니메이션을 시작합니다.
예제 3: 계속해서 회전하는 로고 (AnimatedBuilder
사용)
가장 일반적이고 권장되는 방법인 AnimatedBuilder
를 사용하여 무한히 회전하는 로고를 만들어 보겠습니다.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class ExplicitAnimationExample extends StatefulWidget {
const ExplicitAnimationExample({Key? key}) : super(key: key);
@override
_ExplicitAnimationExampleState createState() => _ExplicitAnimationExampleState();
}
// 1. SingleTickerProviderStateMixin 추가
class _ExplicitAnimationExampleState extends State
with SingleTickerProviderStateMixin {
// 2. 컨트롤러와 애니메이션 변수 선언
late final AnimationController _controller;
@override
void initState() {
super.initState();
// 3. 컨트롤러 초기화
_controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this, // this는 TickerProvider를 의미
)..repeat(); // 생성과 동시에 반복 실행
}
@override
void dispose() {
// 4. 컨트롤러 해제
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('명시적 애니메이션 예제'),
),
body: Center(
// 5. AnimatedBuilder로 UI 빌드
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi, // 0.0~1.0 값을 0~2PI 라디안으로 변환
child: child, // child는 다시 빌드되지 않음
);
},
// 이 child는 builder가 호출될 때마다 다시 생성되지 않으므로 성능에 유리합니다.
child: const FlutterLogo(size: 150),
),
),
);
}
}
코드 분석:
with SingleTickerProviderStateMixin
: 이 믹스인은State
객체에 Ticker를 제공하는 역할을 합니다.AnimationController
의vsync
속성에this
를 전달하기 위해 필수적입니다._controller.repeat()
:initState
에서 컨트롤러를 생성하고 바로repeat()
을 호출하여 애니메이션이 위젯이 생성되자마자 시작되고 무한히 반복되도록 합니다.AnimatedBuilder
: 이 위젯은animation
속성으로_controller
를 구독합니다. 컨트롤러의 값이 변경될 때마다(즉, 매 프레임마다)builder
함수를 다시 호출합니다.builder
함수:_controller.value
는 0.0에서 1.0 사이의 값을 가집니다. 이 값을Transform.rotate
의angle
속성에 사용하기 위해* 2.0 * math.pi
를 곱하여 0에서 360도(2π 라디안) 사이의 값으로 변환합니다.child
속성 최적화:AnimatedBuilder
의child
속성에FlutterLogo
를 전달했습니다. 이렇게 하면builder
가 다시 호출될 때마다FlutterLogo
위젯이 새로 생성되는 것을 방지할 수 있습니다.builder
함수는child
인자를 통해 이 위젯에 접근할 수 있습니다. 이는 애니메이션과 관련 없는 무거운 위젯이 불필요하게 다시 빌드되는 것을 막아 성능을 최적화하는 중요한 기법입니다.
더 간결한 방법: ...Transition
위젯
Flutter는 특정 변환에 대해 AnimatedBuilder
와 Tween
을 미리 조합해 놓은 편리한 위젯들을 제공합니다. 이를 사용하면 코드가 더 간결해집니다.
RotationTransition
: 회전 애니메이션을 적용합니다.ScaleTransition
: 크기(스케일) 애니메이션을 적용합니다.FadeTransition
: 투명도(opacity) 애니메이션을 적용합니다.SlideTransition
: 위치 이동 애니메이션을 적용합니다. (Tween
필요)
예제 4: RotationTransition
으로 회전 로고 재작성
위 예제 3을 RotationTransition
을 사용하면 훨씬 간단하게 만들 수 있습니다.
// ... (State 클래스 선언 및 initState, dispose는 동일)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RotationTransition 예제'),
),
body: Center(
child: RotationTransition(
// turns에 컨트롤러를 직접 전달합니다.
// 컨트롤러의 0.0~1.0 값이 0~1회전으로 자동 매핑됩니다.
turns: _controller,
child: const FlutterLogo(size: 150),
),
),
);
}
AnimatedBuilder
와 Transform.rotate
를 사용했던 부분이 단 하나의 RotationTransition
위젯으로 대체되었습니다. turns
속성은 Animation
타입을 받으며, 컨트롤러의 값이 1.0이 될 때 한 바퀴(360도) 회전을 의미합니다. 훨씬 직관적이고 코드가 깔끔해졌습니다.
명시적 애니메이션 요약
- 장점: 애니메이션의 모든 측면(재생, 정지, 반복, 방향)을 완벽하게 제어할 수 있습니다. 복잡하고 정교한 연출이 가능합니다.
- 단점: 초기 설정이 복잡하고,
AnimationController
와TickerProvider
등 이해해야 할 개념이 많습니다. 코드가 길어집니다.
3부: 암시적 vs 명시적, 언제 무엇을 선택할까?
이제 두 가지 애니메이션 기법을 모두 배웠습니다. 마지막으로 어떤 상황에 어떤 것을 선택해야 할지 명확하게 정리해 보겠습니다.
기준 | 암시적 애니메이션 (Implicit) | 명시적 애니메이션 (Explicit) |
---|---|---|
핵심 개념 | 상태 변경에 대한 자동 전환 | AnimationController를 통한 수동 제어 |
주요 사용 사례 | 일회성 상태 변화 (예: 버튼 클릭 후 크기/색상 변경) | 반복/지속적 애니메이션 (로딩 스피너), 사용자 상호작용 기반 애니메이션 (드래그) |
제어 수준 | 낮음 (시작/정지/반복 제어 불가) | 높음 (재생, 정지, 역재생, 반복, 특정 지점 이동 등 모든 제어 가능) |
코드 복잡도 | 낮음 (setState 만으로 충분) |
높음 (AnimationController , TickerProvider , dispose 등 필요) |
대표 위젯 | AnimatedContainer , AnimatedOpacity , TweenAnimationBuilder |
AnimatedBuilder , RotationTransition , ScaleTransition 등 |
결정 가이드:
- "애니메이션이 계속 반복되어야 하는가?"
- 예: 명시적 애니메이션을 사용하세요. (예:
_controller.repeat()
) - 아니오: 다음 질문으로.
- 예: 명시적 애니메이션을 사용하세요. (예:
- "사용자의 입력(예: 드래그)에 따라 애니메이션이 실시간으로 변해야 하는가?"
- 예: 명시적 애니메이션을 사용하세요. (예: 드래그 거리에 따라
_controller.value
를 조절) - 아니오: 다음 질문으로.
- 예: 명시적 애니메이션을 사용하세요. (예: 드래그 거리에 따라
- "단순히 위젯의 속성이 A에서 B로 한 번만 바뀌면 되는가?"
- 예: 암시적 애니메이션이 완벽한 선택입니다. (예:
AnimatedContainer
) - 아니오: 당신의 요구사항은 아마도 명시적 애니메이션이 필요한 복잡한 시나리오일 가능성이 높습니다.
- 예: 암시적 애니메이션이 완벽한 선택입니다. (예:
결론
Flutter의 애니메이션 시스템은 처음에는 복잡해 보일 수 있지만, 암시적과 명시적이라는 두 가지 핵심 개념을 이해하면 명확한 그림이 그려집니다. 간단하고 빠른 효과를 원할 때는 암시적 애니메이션으로 시작하고, 앱에 더 역동적이고 정교한 생명력을 불어넣고 싶을 때는 명시적 애니메이션의 강력한 제어 기능을 활용하세요.
이 두 가지 도구를 자유자재로 사용할 수 있게 되면, 여러분의 Flutter 앱은 기능적으로 뛰어날 뿐만 아니라 시각적으로도 매력적인, 사용자에게 사랑받는 앱으로 거듭날 것입니다. 이제 배운 내용을 바탕으로 여러분만의 아름다운 애니메이션을 만들어 보세요!
0 개의 댓글:
Post a Comment