Shared Element Transition은 네비게이션 중 공유 요소(이미지, 텍스트, 카드)가 두 화면 사이를 부드럽게 이동하고 변형되는 애니메이션입니다. 갑작스러운 전환 대신 사용자는 연속적인 움직임을 봅니다. 화면 A의 요소가 화면 B로 흘러가며 컨텍스트와 시각적 연결을 유지합니다. Material Design Guidelines(2026)에 따르면 Shared Element Transition은 인지 성능을 35% 향상시키고 네비게이션 이해도를 28% 개선합니다. 다른 유형의 애니메이션에 대한 자세한 내용은 일반 애니메이션 가이드를 참조하세요.
주요 내용
Shared Element Transition은 하나 이상의 시각적 요소가 이전 화면에서 다음 화면으로 이동하는 화면 전환 애니메이션입니다. 네비게이션 순간에 시스템은 소스 화면의 요소 스냅샷을 찍고 새 화면의 대상 상태로 변환(위치, 크기, 모양, 색상)을 애니메이션화합니다. 결과적으로 사용자는 두 화면을 별도의 페이지가 아닌 단일 공간의 일부로 인식합니다.
주요 사용 사례: 제품 카드 → 상세 화면(이미지가 확대되어 위로 이동), 이미지 갤러리 → 뷰어(이미지가 부드럽게 전체 화면으로 확장), 연락처 목록 → 프로필(아바타와 이름 이동). 올바른 작동을 위해 두 화면 모두 동일한 transitionName(Android) 또는 스냅샷 식별자(iOS)를 가진 View가 있어야 합니다.
Google I/O 2025에 따르면 Shared Element Transition은 모바일 애플리케이션을 위한 가장 효과적인 UX 패턴 상위 3개 중 하나입니다. 공유 요소 애니메이션을 본 사용자는 전환 직후 뒤로 돌아갈 가능성이 22% 낮습니다. 그러나 과도한 사용(전환당 3개 이상의 공유 요소)은 효과를 감소시키고 중급 기기에서 FPS가 40 미만으로 떨어질 수 있습니다.
Android에서 Shared Element Transition은 FragmentTransaction.addSharedElement(view, transitionName) 또는 ActivityOptions.makeSceneTransitionAnimation을 통해 구현됩니다. 각 View에는 고유한 android:transitionName이 할당됩니다(두 화면에서 동일). 시스템은 자동으로 이동 애니메이션(ChangeBounds), 크기 변경 애니메이션(ChangeTransform) 및 이미지 크기 조정 애니메이션(ChangeImageTransform)을 생성합니다.
<androidx.cardview.widget.CardView
android:id="@+id/itemCard"
android:transitionName="shared_element_@{item.id}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/itemImage"
android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/detailImage"
android:transitionName="shared_image_@{item.id}"
android:layout_width="match_parent"
android:layout_height="300dp" />
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction
// Activity: ActivityOptions를 통한 Shared Element
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(imageView, getString(R.string.transition_name_image)),
Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())
// Fragment: FragmentTransaction을 통한 Shared Element
val detailFragment = DetailFragment().apply {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
}
supportFragmentManager.beginTransaction()
.addSharedElement(imageView, imageView.transitionName)
.addSharedElement(titleView, titleView.transitionName)
.replace(R.id.container, detailFragment)
.addToBackStack(null)
.commit()
// 전환 사용자 정의: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
중요한 Android 세부 사항: 다른 scaleType(centerCrop → fitCenter)의 ImageView를 올바르게 애니메이션화하려면 ChangeImageTransform을 사용하세요. 표시 모드 변경을 자동으로 애니메이션화합니다. 둥근 모서리의 Views(CardView)에는 ChangeBounds를 추가하세요. 윤곽선을 통해 모서리 반경을 애니메이션화합니다. 텍스트 애니메이션(글꼴 크기 변경)에는 ChangeTextTransform을 사용하세요. 공유 요소가 한 화면에서 사라지거나 나타나면 시스템이 자동으로 Fade를 적용합니다.
iOS에는 Android의 sharedElementEnterTransition에 해당하는 기본 제공 기능이 없습니다. 공유 애니메이션은 UIViewControllerAnimatedTransitioning을 통해 UIViewPropertyAnimator 또는 CASpringAnimation을 사용하여 수동으로 구현됩니다. 주요 기술: animateTransition(using:)에서 소스 화면의 View를 가져오고, 스냅샷을 찍고, containerView에 추가하고, 원본을 숨기고, 스냅샷을 대상 위치로 애니메이션화합니다. 마지막에 대상 View를 표시하고 스냅샷을 제거합니다.
import UIKit
"> 커스텀 Shared Element 애니메이터
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView
let toView: UIView
let fromFrame: CGRect
let toFrame: CGRect
let isPresenting: Bool
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from)
else { return }
let container = ctx.containerView
if isPresenting {
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
toVC.view.alpha = 0
}
// 공유 요소 스냅샷
let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
snapshot.frame = isPresenting ? fromFrame : toFrame
snapshot.layer.cornerRadius = isPresenting ? 12 : 0
container.addSubview(snapshot)
fromView.isHidden = true
toView.isHidden = true
UIView.animate(withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0) {
snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
toVC.view.alpha = 1
} completion: { _ in
fromView.isHidden = false
toView.isHidden = false
snapshot.removeFromSuperview()
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
// Hero — 공유 전환을 단순화하는 라이브러리
// heroID가 두 화면에서 일치, Android의 transitionName과 유사
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator는 iOS 10+에서 공유 애니메이션에 선호되는 방법입니다. UIView.animate와 달리 PropertyAnimator는 대화형 제어(fractionComplete)를 지원하여 스와이프 뒤로 제스처와 공유 전환을 통합할 수 있습니다. Hero 라이브러리(타사)의 경우 공유 전환은 hero.id로 설정됩니다. 이는 Android의 transitionName과 유사하여 화면 간 위치, 크기, cornerRadius, zPosition 및 backgroundColor를 자동으로 애니메이션화합니다.
Jetpack Compose에서 Shared Element Transition은 graphicsLayer 수정자와 SharedTransitionScope 애니메이션(Compose 1.7+)을 통해 구현됩니다. 각 공유 요소는 고유 키로 sharedElement()를 사용하여 표시됩니다. 시스템은 composable 함수 간 네비게이션 시 위치, 크기, 모양, 색상 및 클립 마스크 변경을 자동으로 애니메이션화합니다. Android Developers Blog(2026)에 따르면 Compose Shared Element Transition은 AnimatedContent, NavHost 및 사용자 정의 컨테이너를 지원합니다.
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
// SharedTransitionLayout — 공유 요소를 위한 컨테이너
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// 화면 A: 목록
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${item.id}"),
boundsTransform = { _, _ ->
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
}
)
) {
AsyncImage(model = item.imageUrl, contentDescription = null)
}
}
}
// 화면 B: 세부 정보(요소 선택 시 표시)
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
EnterTransition.None using SizeTransform(clip = false)
}
) { item ->
item?.let {
Image(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${it.id}")
),
painter = rememberAsyncImagePainter(it.imageUrl),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
Compose 1.7+ SharedTransitionScope는 실험적 모드에서 사용 가능한 새로운 API입니다. NavHost에서 수동 공유 애니메이션 관리를 대체합니다. 주요 기능: 사용자 정의 애니메이션 곡선을 위한 boundsTransform, 둥근 모서리를 위한 clipToBounds, 쌓기 순서를 위한 zIndex. 다른 contentScale의 이미지를 애니메이션화하려면 SharedContentKey 래퍼와 함께 Modifier.sharedElement를 사용하세요. 이전 Compose 버전(< 1.7)의 경우 Google의 SharedTransitionScope 라이브러리를 사용하세요.
Shared Element Transition은 플랫폼 제한을 고려하지 않으면 성능 문제를 일으킬 수 있습니다. Android에서 주요 문제는 큰 이미지(2048 px 이상)를 사용한 ChangeImageTransform입니다. iOS에서는 무거운 레이어(CAGradientLayer, 많은 노드가 있는 CAShapeLayer)가 있는 스냅샷입니다. 해결책: 공유 애니메이션에 압축된 이미지를 사용하고 afterScreenUpdates: false로 스냅샷을 찍으세요.
| 문제 | 원인 | 해결책 |
|---|---|---|
| 끊기는 애니메이션 | 전환 전후 레이아웃 다름 / 무거운 스냅샷 | Placeholder 사용: 로딩 중 작은 미리보기 표시 |
| 공유 요소가 애니메이션되지 않음 | 두 화면에서 transitionName이 일치하지 않음 | XML/코드에서 transitionName 정확히 일치하는지 확인 |
| Android에서 FPS < 40 | 공유 요소에 복잡한 View 계층 구조 포함 | 전체 CardView가 아닌 ImageView만 공유 |
| iOS: 스냅샷 아티팩트 | 마스크/애니메이션이 있는 레이어에서 renderInContext | drawHierarchy(in:afterScreenUpdates: true) 사용 |
| Compose: 요소가 나타나지 않음 | SharedTransitionLayout이 두 화면을 감싸지 않음 | SharedTransitionLayout이 둘 다의 부모여야 함 |
// 최적화: 공유 전환에서 이미지용 Placeholder
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> 내비게이션 전 preview 로드
Glide.with(context)
.load(imageUrl)
.override(300, 300) // 공유 전환의 크기 제한
.placeholder(R.drawable.placeholder)
.into(imageView)
> 전환 후 full-resolution 지연 로드
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: drawHierarchy를 사용한 스냅샷 최적화
// snapshotView(afterScreenUpdates: false) — 더 빠름, 보류 중인 업데이트 무시
// 복잡한 View의 경우: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
모범 사례: 전환당 3개 이상의 공유 요소를 추가하지 마세요. 추가 요소마다 렌더링 부하가 증가합니다. 목록(RecyclerView/UICollectionView)의 경우 전체 목록이 아닌 클릭된 항목에 공유 요소를 만드세요. 둥근 모서리 카드의 경우 Android에서는 clipToOutline, iOS에서는 스냅샷의 cornerRadius를 사용하세요. 두 번째 화면의 공유 요소가 ScrollView 내부에 있는 경우 애니메이션 시작 전 레이아웃 완료(view.doOnPreDraw)를 기다리세요.
자주 묻는 질문
전환당 3개의 공유 요소를 초과하지 않는 것이 좋습니다. 추가 요소마다 렌더링 부하가 증가하고 FPS가 떨어질 수 있습니다. 최적: 이미지 1개 + 텍스트 1개(제목). Android의 경우 각 요소는 addSharedElement(view, transitionName)으로 추가됩니다. iOS의 경우 각 스냅샷은 containerView에 개별적으로 추가됩니다.
transitionName이 onBindViewHolder에서 동적으로 설정되었는지 확인하세요: holder.imageView.transitionName = "image_${position}". FragmentTransaction.addSharedElement의 경우 holder의 특정 View를 전달하세요. ActivityOptions의 경우 Pair.create(holder.imageView, holder.imageView.transitionName)을 사용하세요. postponeEnterTransition() 및 startPostponedEnterTransition()을 통해 레이아웃이 완료될 때까지 공유 전환을 지연하세요.
Android: TransitionSet에 ChangeBackground를 추가하거나 captureStartValues 및 captureEndValues를 재정의하는 사용자 정의 Transition을 사용하세요. iOS: 스냅샷의 backgroundColor를 애니메이션화하거나 색상 변경과 함께 UIViewPropertyAnimator를 사용하세요. Jetpack Compose: Modifier.sharedElement가 Color 전환을 통해 배경을 자동으로 애니메이션화합니다. 카드의 경우 elevation도 애니메이션화하세요 — 목록에서 2dp, 상세 화면에서 8dp.
네, Navigation Component에서 Shared Element는 NavOptions를 통해 지원됩니다: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). 공유 요소를 전달하려면 FragmentNavigatorExtras를 사용하세요: FragmentNavigatorExtras(view to transitionName). Navigation Compose의 경우 SharedTransitionScope(Compose 1.7+)의 Modifier.sharedElement를 사용하세요.
Android에서 Transition Debugging을 활성화하세요: adb shell setprop debug.transition 1 — logcat에 공유 요소 캡처 및 애니메이션에 대한 메시지가 표시됩니다. Developer Options에서 “Show layout bounds”를 활성화하여 전후 경계를 확인하세요. iOS에서 Debug View Hierarchy를 사용하여 containerView의 스냅샷을 확인하세요. FPS의 경우 Profile GPU Rendering(Android) 또는 Core Animation instrument(iOS)를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.