ABOUT ME

-

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

    겹쳐 놓인 색종이 조각 중 하나를 들어내는 손
    배지 하나 움직였는데 무지개로 물든 화면

    알림 배지에 숫자가 하나 늘어나는 작은 애니메이션을 넣었습니다.
    화면 오른쪽 위에 뜨는 동그라미 하나, 숫자가 슥 올라오는 정도의 소소한 연출이었습니다.
    그런데 그 배지가 움직일 때마다 아래에 깔린 지도가 미세하게 버벅였습니다.
    배지는 화면의 아주 작은 구석인데, 왜 지도 전체가 같이 흔들리는지 한동안 이해가 안 됐습니다.

    배지는 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
    반응형
Designed by Tistory.