-
Positioned 애니메이션이 프레임을 태우는 이유AI Agent/flutter 2026. 7. 5. 09:00728x90반응형

배지 하나 움직였는데 무지개로 물든 화면 알림 배지에 숫자가 하나 늘어나는 작은 애니메이션을 넣었습니다.
화면 오른쪽 위에 뜨는 동그라미 하나, 숫자가 슥 올라오는 정도의 소소한 연출이었습니다.
그런데 그 배지가 움직일 때마다 아래에 깔린 지도가 미세하게 버벅였습니다.
배지는 화면의 아주 작은 구석인데, 왜 지도 전체가 같이 흔들리는지 한동안 이해가 안 됐습니다.배지는 Stack 안에서 Positioned로 자리를 잡고 있었고, 애니메이션은 그 top 값을 매 프레임 바꾸는 방식이었습니다.
겉보기엔 배지 하나만 움직이는데, Flutter 입장에서는 그게 그렇게 간단한 일이 아니었습니다.배지만 움직였는데 왜 전체가 흔들릴까요
Positioned의 값을 바꾸면 Stack은 자식들의 배치를 다시 계산해야 합니다.
위치가 바뀌었으니 레이아웃을 다시 재는 건 어찌 보면 당연합니다.
문제는 이 계산이 매 프레임 일어난다는 점입니다.
60fps라면 1초에 예순 번, 배지 하나 움직이자고 Stack 전체가 예순 번 자를 다시 대는 셈입니다.// 값이 바뀔 때마다 Stack의 layout을 다시 타는 방식 Positioned( top: _slide.value, right: 12, child: NotificationBadge(count: _count), )저는 이게 그렇게 비싼 줄 모르고 있었습니다.
배지가 작으니 비용도 작을 거라고 막연히 생각했는데, 위젯의 크기와 다시 그리는 비용은 별개였습니다.
작은 위젯도 잘못된 자리에서 움직이면 큰 청구서를 남깁니다.처음엔 지도 위젯 쪽을 의심했습니다.
지도는 원래 무거운 물건이니까, 타일을 다시 불러오거나 하는 줄 알고 지도 렌더링 옵션을 한참 뒤졌습니다.
지도는 아무 잘못이 없었습니다.
배지가 애니메이션을 도는 동안에만 지도가 버벅였고, 배지를 멈추면 지도는 멀쩡했습니다.
범인은 무거운 지도가 아니라, 그 위에 얹힌 가벼운 배지 쪽이었습니다.
이 순서를 뒤집어 생각하는 데 시간을 꽤 썼습니다.무지개로 물든 화면이 말해준 것
원인을 눈으로 보려고 repaint rainbow를 켰습니다.
다시 그려지는 영역에 색이 입혀지고, 매 프레임 색이 돌면 무지개처럼 보이는 디버그 옵션입니다.
저는 배지 근처만 알록달록해질 거라고 예상했습니다.켠 순간 화면 전체가 무지개였습니다.
배지가 한 번 움직일 때마다 지도도, 그 위의 버튼도, 하단 바까지 죄다 색이 돌고 있었습니다.
작은 동그라미 하나가 화면 전부를 다시 칠하게 만들고 있었던 겁니다.
그 알록달록한 화면을 보고서야 "아, 배지가 문제가 아니라 배지가 앉은 자리가 문제구나" 싶었습니다.repaint 영역이 배지를 넘어 부모까지 번진 이유는, layout이 다시 돌면 그 위에 얹힌 paint도 함께 무효가 되기 때문입니다.
경계선이 없으면 무효 표시는 위로, 옆으로 계속 퍼집니다.Positioned를 걷어내자 프레임이 돌아왔습니다
고친 건 두 가지였습니다.
먼저 단순히 위치만 옮기는 애니메이션은 layout을 건드리지 않는 Transform으로 바꿨습니다.
Transform.translate는 자리를 다시 재지 않고, 그리는 단계에서 위치만 살짝 밀어줍니다.
그리고 배지를 RepaintBoundary로 감싸서, 배지가 다시 그려져도 그 무효 표시가 부모 지도까지 번지지 않게 벽을 세웠습니다.RepaintBoundary( child: Transform.translate( offset: Offset(0, _slide.value), child: const NotificationBadge(), ), )다시 repaint rainbow를 켜보니 이번엔 배지 주변만 색이 돌았습니다.
지도는 조용했습니다.
버벅임도 체감상 사라졌습니다.
정확히 몇 밀리초를 아꼈는지 재보진 않았지만, 손가락으로 배지를 몇 번 올려보는 것만으로도 확실히 매끄러워진 게 느껴졌습니다.둘 중에 뭐가 더 결정적이었나 따져보면, 저는 RepaintBoundary 쪽이었다고 봅니다.
Transform으로 layout을 안 타게 만든 것도 물론 컸지만, 그것만으로는 배지가 다시 그려질 때 무효 표시가 부모로 번지는 걸 막지 못합니다.
경계선을 하나 세워서 "여기까지만 다시 그려라" 하고 못을 박은 게, 지도를 조용하게 만든 진짜 이유였습니다.
다만 RepaintBoundary도 공짜는 아니어서, 아무 데나 남발하면 오히려 레이어가 늘어 손해입니다.
그래서 지금은 자주 혼자 움직이는 위젯 앞에만 골라서 세웁니다.지금은 움직이는 UI를 짤 때 제일 먼저 스스로에게 묻습니다.
이건 자리를 다시 재야 하는 움직임인가, 아니면 그냥 옮겨 보이기만 하면 되는 움직임인가.
둘은 비슷해 보여도 청구서가 다르고, 저는 그 차이를 배지 하나 때문에 무지개 화면을 마주하고서야 손에 익혔습니다.원문 기준
원문 기준: https://docs.flutter.dev/perf/best-practices
원문 본 날짜: 2026-07-01.
728x90반응형'AI Agent > flutter' 카테고리의 다른 글
Flutter 라우팅은 화면 이동이 아니라 상태와 권한의 계약입니다 (0) 2026.07.05 SafeArea는 항상 안전하지 않습니다 (0) 2026.07.05 화면 밖으로 나간 Flutter 상태가 언제 살아남는지 정해야 합니다 (0) 2026.07.04 Flutter 모델 생성 코드는 소스가 아니라 산출물입니다 (0) 2026.07.04 context는 전역 변수가 아니라 트리 안의 위치입니다 (0) 2026.07.04