Flutter 튜토리얼 26편: Hero 애니메이션과 페이지 전환

요약#

핵심 요지#

  • 문제 정의: 화면 전환 시 관련 요소가 갑자기 사라지고 나타나면 사용자 경험이 단절된다.
  • 핵심 주장: Hero 위젯으로 화면 간 요소를 자연스럽게 연결하면 시각적 연속성을 제공한다.
  • 주요 근거: Hero1 위젯과 동일한 tag2만 지정하면 Flutter가 자동으로 전환 애니메이션을 처리한다.
  • 실무 기준: 이미지 갤러리, 상품 목록, 프로필 카드 등에서 효과적으로 활용된다.
  • 한계: 복잡한 위젯 구조나 다른 형태 간 전환은 추가 구현이 필요하다.

문서가 설명하는 범위#

  • Hero 위젯의 기본 사용법
  • 화면 간 공유 요소 전환 구현
  • 비행 경로와 애니메이션 커스터마이징
  • 원형-사각형 변환 애니메이션

읽는 시간: 12분 | 난이도: 중급


참고 자료#


문제 상황#

모바일 앱에서 목록의 항목을 탭하면 상세 화면으로 이동합니다.
이때 항목이 갑자기 사라지고 새 화면이 나타나면 사용자는 두 화면의 연결성을 인지하기 어렵습니다.

화면 전환의 맥락 단절#

목록 화면 상세 화면
┌─────────────┐ ┌─────────────┐
│ ┌─────┐ │ │ │
│ │이미지│ 탭 │ ──갑자기──→│ ┌───────┐ │
│ └─────┘ │ 전환 │ │ 이미지 │ │
│ 제목 │ │ └───────┘ │
└─────────────┘ │ 설명... │
└─────────────┘

문제는 다음과 같습니다.

  • 작은 이미지가 갑자기 사라지고 큰 이미지가 갑자기 나타난다.
  • 사용자가 어떤 항목을 선택했는지 시각적으로 확인하기 어렵다.
  • 두 화면 간의 맥락이 끊어져 앱이 투박하게 느껴진다.

해결 방법#

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를 연결해야 할지 알 수 없습니다.

// ❌ 오류: 같은 화면에 동일 tag
Column(
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#

NOTE

Hero가 어떻게 동작하는지 이해하면 문제가 생겼을 때 원인을 찾기 쉽습니다.
또한 커스터마이징할 때도 올바른 방향을 잡을 수 있습니다.

핵심 질문: Hero는 정말로 “날아가는” 것일까요?

What#

NOTE

Hero는 실제로 소스에서 목적지로 이동하는 것이 아닙니다.
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 ╮
╰→ B

Material 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#

WARNING

placeholderBuilder로 빈 공간 채우기

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

ClipOvalClipRect를 조합하면 원형↔사각형 변환 효과를 만들 수 있습니다.

원리:

  • 작은 크기에서는 ClipOval이 지배적 → 원형으로 보임
  • 큰 크기에서는 ClipRect가 지배적 → 사각형으로 보임
  • 중간 크기에서는 둘의 교차 영역 → 점진적 변환

How#

TIP

RadialExpansion 위젯 만들기

import 'dart:math' as math;
class RadialExpansion extends StatelessWidget {
final double maxRadius;
final Widget child;
const RadialExpansion({
required this.maxRadius,
required this.child,
});
@override
Widget 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(...),
)

결론: ClipOvalClipRect 조합으로 원형↔사각형 변환 애니메이션을 구현합니다.


챕터 6: 페이지 전환과 Hero 조합하기#

Why#

NOTE

Hero 애니메이션은 페이지 전환과 별개로 동작합니다.
즉, 페이지가 슬라이드/페이드되는 동안 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#

  1. Hero(히어로): Flutter에서 화면 간 공유 요소 전환을 구현하는 위젯이다. 같은 tag를 가진 두 Hero 사이에 자동으로 비행 애니메이션이 생성된다.

  2. tag(태그): Hero 위젯을 식별하는 고유한 값이다. 소스 화면과 목적지 화면의 Hero가 같은 tag를 가져야 전환 애니메이션이 동작한다.

  3. 공유 요소 전환(Shared Element Transition): 두 화면 사이에서 같은 요소가 자연스럽게 이동하는 것처럼 보이는 애니메이션 기법이다.

  4. Navigator(네비게이터): Flutter에서 화면 전환을 관리하는 위젯이다. push()로 새 화면을 열고, pop()으로 이전 화면으로 돌아간다.

  5. Overlay(오버레이): Flutter에서 다른 위젯들 위에 떠 있는 투명 레이어다. Hero 애니메이션 중 위젯은 이 Overlay에서 비행한다.

  6. MaterialRectArcTween(머티리얼렉트아크트윈): 직선이 아닌 호(곡선)를 그리며 사각형 영역을 보간하는 클래스다.

  7. PageRouteBuilder(페이지라우트빌더): 페이지 전환 애니메이션을 커스터마이징할 수 있는 Flutter 클래스다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 26편: Hero 애니메이션과 페이지 전환
https://moodturnpost.net/posts/flutter/flutter-hero-animations/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차