Flutter 튜토리얼 26편: Hero 애니메이션과 페이지 전환
요약
핵심 요지
문서가 설명하는 범위
- Hero 위젯의 기본 사용법
- 화면 간 공유 요소 전환 구현
- 비행 경로와 애니메이션 커스터마이징
- 원형-사각형 변환 애니메이션
읽는 시간: 12분 | 난이도: 중급
참고 자료
- Hero animations - Hero 애니메이션 공식 문서
- Navigate with named routes - 화면 전환 방법
- PageRouteBuilder - 커스텀 페이지 전환
문제 상황
모바일 앱에서 목록의 항목을 탭하면 상세 화면으로 이동합니다.
이때 항목이 갑자기 사라지고 새 화면이 나타나면 사용자는 두 화면의 연결성을 인지하기 어렵습니다.
화면 전환의 맥락 단절
목록 화면 상세 화면┌─────────────┐ ┌─────────────┐│ ┌─────┐ │ │ ││ │이미지│ 탭 │ ──갑자기──→│ ┌───────┐ ││ └─────┘ │ 전환 │ │ 이미지 │ ││ 제목 │ │ └───────┘ │└─────────────┘ │ 설명... │ └─────────────┘문제는 다음과 같습니다.
- 작은 이미지가 갑자기 사라지고 큰 이미지가 갑자기 나타난다.
- 사용자가 어떤 항목을 선택했는지 시각적으로 확인하기 어렵다.
- 두 화면 간의 맥락이 끊어져 앱이 투박하게 느껴진다.
해결 방법
Flutter의 Hero 위젯은 화면 간 요소를 자연스럽게 연결하는 공유 요소 전환3을 제공합니다.
동일한 tag를 가진 Hero 위젯이 다른 화면에 존재하면, Navigator4가 자동으로 비행 애니메이션을 생성합니다.
챕터 1: Hero 위젯 기본 이해하기
Why
NOTE화면 간 요소가 “날아가는” 것처럼 보이면 사용자는 두 화면의 연결성을 직관적으로 이해합니다.
예를 들어, 썸네일 이미지가 상세 화면의 큰 이미지로 부드럽게 변하면 “같은 사진”임을 바로 알 수 있습니다.이를 구현하려면 양쪽 화면에서 같은 요소를 식별할 방법이 필요합니다.
Flutter는tag라는 고유 식별자로 이 문제를 해결합니다.graph LR A[소스 Hero<br>tag: 'photo-1'] -->|Navigator.push| B[목적지 Hero<br>tag: 'photo-1'] B -->|Navigator.pop| A
What
NOTE
Hero위젯은 화면 전환 시 자동으로 비행 애니메이션을 생성합니다.핵심 원리:
- 소스 화면의 Hero와 목적지 화면의 Hero가 같은 tag를 가지면 연결됩니다.
Navigator.push()를 호출하면 소스에서 목적지로 “날아갑니다”.Navigator.pop()을 호출하면 목적지에서 소스로 “돌아옵니다”.
How
TIP기본 사용법은 매우 간단합니다.
1단계: 소스 화면에서 Hero로 감싸기
// 소스 화면 - 작은 이미지Hero(tag: 'photo-1', // 고유한 태그child: Image.asset('assets/photo.png', width: 100),)2단계: 목적지 화면에서 같은 tag로 Hero 감싸기
// 목적지 화면 - 큰 이미지Hero(tag: 'photo-1', // 소스와 동일한 태그!child: Image.asset('assets/photo.png', width: 300),)이것이 전부입니다.
Navigator.push()로 화면을 전환하면 이미지가 100px에서 300px로 부드럽게 커지며 날아갑니다.tag가 하는 일:
tag 값 소스 화면 목적지 화면 결과 ’photo-1’ Hero 있음 Hero 있음 ✅ 애니메이션 동작 ’photo-1’ vs ‘photo-2’ Hero 있음 Hero 있음 ❌ 연결 안 됨 ’photo-1’ Hero 있음 Hero 없음 ❌ 애니메이션 없음
Watch out
WARNING같은 화면에 동일한 tag가 있으면 오류가 발생합니다.
Flutter는 한 화면에서 tag가 중복되면 어떤 Hero를 연결해야 할지 알 수 없습니다.
// ❌ 오류: 같은 화면에 동일 tagColumn(children: [Hero(tag: 'photo', child: Image.asset('a.png')),Hero(tag: 'photo', child: Image.asset('b.png')), // 충돌!],)해결: 각 항목에 고유한 tag를 부여합니다.
// ✅ 올바른 예Hero(tag: 'photo-1', child: Image.asset('a.png')),Hero(tag: 'photo-2', child: Image.asset('b.png')),
결론: Hero 위젯과 동일한 tag만 있으면 화면 간 비행 애니메이션이 자동으로 동작합니다.
챕터 2: Hero 애니메이션의 동작 원리
Why
NOTEHero가 어떻게 동작하는지 이해하면 문제가 생겼을 때 원인을 찾기 쉽습니다.
또한 커스터마이징할 때도 올바른 방향을 잡을 수 있습니다.핵심 질문: Hero는 정말로 “날아가는” 것일까요?
What
NOTEHero는 실제로 소스에서 목적지로 이동하는 것이 아닙니다.
Flutter는Overlay5라는 별도 레이어를 사용합니다.동작 순서:
graph TD A["1. Navigator.push() 호출"] --> B["2. 목적지 Hero 위치 계산"] B --> C["3. 소스 Hero를 Overlay로 복사"] C --> D["4. 소스 Hero 숨김"] D --> E["5. Overlay에서 목적지 위치로 비행"] E --> F["6. 목적지 Hero 표시, Overlay 제거"]쉽게 말하면:
- 소스 이미지가 화면에서 “복사”되어 투명 레이어 위로 올라갑니다.
- 투명 레이어 위에서 목적지 위치까지 날아갑니다.
- 도착하면 목적지 이미지가 나타나고 투명 레이어의 복사본은 사라집니다.
How
TIP디버깅 팁: 애니메이션을 느리게 보기
애니메이션이 너무 빨라서 동작을 확인하기 어려울 때 사용합니다.
import 'package:flutter/scheduler.dart';void main() {timeDilation = 5.0; // 5배 느리게 (1.0이 정상 속도)runApp(MyApp());}이렇게 하면 Hero가 천천히 날아가는 모습을 볼 수 있습니다.
개발이 끝나면 반드시 제거하거나 1.0으로 되돌리세요.
Watch out
WARNING애니메이션 중에는 Hero가 Overlay에 있습니다.
이 말은 Hero가 비행 중일 때 소스 화면이나 목적지 화면의 위젯 트리에 속하지 않는다는 뜻입니다.
따라서:
- 비행 중에는 Hero를 탭해도 반응하지 않을 수 있습니다.
- 화면의 다른 위젯과 겹치면 Hero가 위에 표시됩니다.
결론: Hero는 Overlay를 통해 두 화면 위에서 비행하며, 이 원리를 알면 디버깅이 쉬워집니다.
챕터 3: 리스트에서 상세 화면으로 전환하기
Why
NOTE실제 앱에서 Hero는 주로 리스트 → 상세 화면 패턴에 사용됩니다.
예: 갤러리에서 사진 탭 → 사진 상세 보기리스트에는 여러 항목이 있으므로 각 항목마다 고유한 tag가 필요합니다.
보통 항목의 ID를 tag로 사용합니다.
What
NOTE리스트의 각 항목에 고유한 tag를 부여하는 패턴입니다.
tag 생성 패턴: 'photo-$id'리스트 항목 tag----------- ----사진 1 'photo-1'사진 2 'photo-2'사진 3 'photo-3'
How
TIP핵심 코드만 보면 이렇습니다.
1단계: 데이터 클래스 정의
class Photo {final String id;final String url;const Photo({required this.id, required this.url});}2단계: 리스트 항목에서 Hero 사용
// 리스트의 각 항목Hero(tag: 'photo-${photo.id}', // 'photo-1', 'photo-2', ...child: Image.asset(photo.url),)3단계: 상세 화면에서 같은 패턴으로 tag 생성
// 상세 화면Hero(tag: 'photo-${photo.id}', // 리스트와 동일한 패턴!child: Image.asset(photo.url),)자주 쓰는 tag 패턴:
상황 tag 패턴 예시 사진 갤러리 'photo-$id''photo-123'상품 목록 'product-$id''product-456'사용자 프로필 'user-$userId''user-789'
Watch out
WARNING네트워크 이미지 사용 시 주의
네트워크 이미지는 로딩 시간이 있어서 Hero 전환 시 깜빡임이 생길 수 있습니다.
해결책: 양쪽 화면에서 같은 이미지를 캐싱하거나, 로딩 상태를 동일하게 유지합니다.
// 양쪽 화면에서 동일한 설정 사용Image.network(imageUrl,fit: BoxFit.cover, // 양쪽 동일cacheWidth: 300, // 양쪽 동일)
결론: 리스트의 각 항목에 'type-$id' 패턴으로 고유 tag를 부여하면 어떤 항목이든 Hero 전환이 가능합니다.
챕터 4: 비행 경로 커스터마이징
Why
NOTE기본 Hero 애니메이션은 직선으로 이동합니다.
하지만 디자인에 따라 곡선 경로가 더 자연스러울 수 있습니다.직선 경로 (기본) 곡선 경로 (커스텀)A ────────→ B A ╮│╰→ BMaterial Design에서는 곡선 경로를 권장합니다.
What
NOTE
createRectTween속성으로 비행 경로를 변경할 수 있습니다.
Tween 클래스 경로 형태 언제 사용? RectTween직선 기본값 MaterialRectArcTween6호 (곡선) Material 앱 MaterialRectCenterArcTween중심점 기준 호 원형 전환
How
TIP곡선 경로로 변경하기
Hero(tag: 'photo-1',createRectTween: (begin, end) {// 직선 대신 호를 그리며 이동return MaterialRectArcTween(begin: begin, end: end);},child: Image.asset('photo.png'),)이 한 줄만 추가하면 Hero가 부드러운 곡선을 그리며 날아갑니다.
비행 중 크기 변화 효과 추가하기
Hero(tag: 'photo-1',flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext,) {// 비행 중 살짝 커졌다가 작아지는 효과return ScaleTransition(scale: animation.drive(Tween(begin: 1.0, end: 1.1).chain(CurveTween(curve: Curves.easeInOut)),),child: fromContext.widget,);},child: Image.asset('photo.png'),)
Watch out
WARNINGplaceholderBuilder로 빈 공간 채우기
Hero가 날아가면 소스 위치가 비어 보입니다.
이때placeholderBuilder로 임시 위젯을 표시할 수 있습니다.Hero(tag: 'photo-1',placeholderBuilder: (context, heroSize, child) {// Hero가 날아간 자리에 회색 박스 표시return Container(width: heroSize.width,height: heroSize.height,color: Colors.grey[300],);},child: Image.asset('photo.png'),)
결론: createRectTween으로 비행 경로를, flightShuttleBuilder로 비행 중 모양을 커스터마이징합니다.
챕터 5: 원형에서 사각형으로 변환하기
Why
NOTE프로필 아바타처럼 원형 요소가 상세 화면에서 사각형으로 변하는 경우가 있습니다.
단순히 크기만 변하면 어색합니다.
형태도 함께 변해야 자연스럽습니다.원형 아바타 → 사각형 프로필○ ┌────┐(50px) │ │└────┘(200px)
What
NOTE
ClipOval과ClipRect를 조합하면 원형↔사각형 변환 효과를 만들 수 있습니다.원리:
- 작은 크기에서는 ClipOval이 지배적 → 원형으로 보임
- 큰 크기에서는 ClipRect가 지배적 → 사각형으로 보임
- 중간 크기에서는 둘의 교차 영역 → 점진적 변환
How
TIPRadialExpansion 위젯 만들기
import 'dart:math' as math;class RadialExpansion extends StatelessWidget {final double maxRadius;final Widget child;const RadialExpansion({required this.maxRadius,required this.child,});@overrideWidget build(BuildContext context) {// 원형 클리핑 안에 사각형 클리핑return ClipOval(child: Center(child: SizedBox(width: 2.0 * maxRadius / math.sqrt2,height: 2.0 * maxRadius / math.sqrt2,child: ClipRect(child: child),),),);}}사용 방법:
// 소스: 작은 원형 (radius 50)Hero(tag: 'avatar',child: RadialExpansion(maxRadius: 50,child: Image.asset('avatar.png'),),)// 목적지: 큰 사각형 (radius 150)Hero(tag: 'avatar',child: RadialExpansion(maxRadius: 150,child: Image.asset('avatar.png'),),)
maxRadius가 작으면 원형에 가깝고, 크면 사각형에 가까워집니다.
Watch out
WARNING중심점 기준 경로 사용하기
원형↔사각형 전환에서는
MaterialRectCenterArcTween이 더 자연스럽습니다.
모서리가 아닌 중심점 기준으로 호를 그리기 때문입니다.Hero(tag: 'avatar',createRectTween: (begin, end) {return MaterialRectCenterArcTween(begin: begin, end: end);},child: RadialExpansion(...),)
결론: ClipOval과 ClipRect 조합으로 원형↔사각형 변환 애니메이션을 구현합니다.
챕터 6: 페이지 전환과 Hero 조합하기
Why
NOTEHero 애니메이션은 페이지 전환과 별개로 동작합니다.
즉, 페이지가 슬라이드/페이드되는 동안 Hero는 독립적으로 날아갑니다.이를 활용하면 더 풍부한 전환 효과를 만들 수 있습니다.
기본 전환: 페이지 슬라이드 + Hero 비행 (기본 300ms)커스텀 전환: 페이지 페이드 + Hero 비행 (시간 조절 가능)
What
NOTE
PageRouteBuilder7를 사용하면 페이지 전환 애니메이션을 자유롭게 정의할 수 있습니다.
Hero는 자동으로 이 전환과 함께 동작합니다.
How
TIP페이드 전환 + Hero
Navigator.push(context,PageRouteBuilder(transitionDuration: Duration(milliseconds: 500),pageBuilder: (context, animation, secondaryAnimation) {return DetailScreen();},transitionsBuilder: (context, animation, secondaryAnimation, child) {// 페이지는 페이드로 나타나고, Hero는 날아감return FadeTransition(opacity: animation, child: child);},),);슬라이드 + 페이드 조합
transitionsBuilder: (context, animation, secondaryAnimation, child) {final offsetAnimation = animation.drive(Tween(begin: Offset(0.0, 0.1), end: Offset.zero),);return SlideTransition(position: offsetAnimation,child: FadeTransition(opacity: animation, child: child),);}전환 효과 비교:
전환 방식 효과 Hero와 조합 MaterialPageRoute오른쪽에서 슬라이드 기본 FadeTransition페이드 인/아웃 부드러움 SlideTransition방향 지정 슬라이드 역동적 ScaleTransition크기 변화 강조 효과
Watch out
WARNING전환 시간 맞추기
Hero의 기본 애니메이션 시간은 약 300ms입니다.
페이지 전환 시간을 너무 다르게 설정하면 어색해 보입니다.PageRouteBuilder(// 페이지 전환도 300ms로 맞추기transitionDuration: Duration(milliseconds: 300),reverseTransitionDuration: Duration(milliseconds: 300),// ...)
결론: PageRouteBuilder로 페이지 전환을 커스터마이징하면 Hero와 함께 더 풍부한 UX를 제공합니다.
한계
Hero 애니메이션은 강력하지만 모든 상황에 적합하지는 않습니다.
- 복잡한 위젯 구조: Hero 내부의 위젯 트리가 양쪽 화면에서 크게 다르면 전환이 어색해질 수 있습니다.
- 다중 Hero 충돌: 같은 화면에 동일 tag의 Hero가 있으면 오류가 발생합니다.
- 성능: 매우 복잡한 위젯을 Hero로 감싸면 애니메이션이 끊길 수 있습니다.
- 형태 변환: 원형↔사각형 외의 복잡한 형태 변환은 추가 구현이 필요합니다.
Footnotes
-
Hero(히어로): Flutter에서 화면 간 공유 요소 전환을 구현하는 위젯이다. 같은 tag를 가진 두 Hero 사이에 자동으로 비행 애니메이션이 생성된다. ↩
-
tag(태그): Hero 위젯을 식별하는 고유한 값이다. 소스 화면과 목적지 화면의 Hero가 같은 tag를 가져야 전환 애니메이션이 동작한다. ↩
-
Overlay(오버레이): Flutter에서 다른 위젯들 위에 떠 있는 투명 레이어다. Hero 애니메이션 중 위젯은 이 Overlay에서 비행한다. ↩
-
MaterialRectArcTween(머티리얼렉트아크트윈): 직선이 아닌 호(곡선)를 그리며 사각형 영역을 보간하는 클래스다. ↩
-
PageRouteBuilder(페이지라우트빌더): 페이지 전환 애니메이션을 커스터마이징할 수 있는 Flutter 클래스다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!