# 2026년 Flutter 성능 최적화: Impeller, 리빌드, 모범 사례 > 2026년에 Flutter 앱을 60 또는 120fps로 안정적으로 유지하는 방법. Impeller, 절제된 위젯 리빌드, RepaintBoundary, DevTools 프로파일링을 다룹니다. - Published: 2026-07-04 - Updated: 2026-07-07 - Author: SharpSkill - Tags: flutter, performance, impeller, best-practices, dart, mobile - Reading time: 9 min --- 2026년 Flutter 성능 최적화는 부드러운 앱을 만드는 방식을 바꾼 두 가지 전환에 기대고 있습니다. 렌더링 엔진 Impeller가 iOS와 Android에서 기본값으로 제공되기 시작했다는 점, 그리고 프레임워크의 리빌드 모델이 위젯 트리를 작고 불변으로 유지하는 개발자에게 보상을 준다는 점입니다. 이 두 지렛대에 기대는 팀은 프레임마다 손으로 조정하지 않고도 60 또는 120fps를 안정적으로 유지합니다. > **가장 영향이 큰 변화** > > 대부분의 Flutter 앱에서 가장 큰 이득은 `const` 생성자를 추가하고 큰 `build` 메서드를 분할하는 것입니다. 그러면 `setState` 호출이 화면 전체가 아니라 버튼 하나만 다시 그립니다. ## Impeller가 Flutter 성능의 기준선을 다시 세우는 이유 Impeller는 Flutter의 렌더링 엔진이며, 모바일 플랫폼 전반에서 Skia를 대신해 기본값이 되었습니다. 차이가 가장 크게 드러나는 곳은 애니메이션의 처음 몇 초입니다. Skia는 셰이더를 저스트 인 타임으로, 특정 효과가 화면에 처음 나타날 때 컴파일했고, 이것이 Flutter 앱이 비판받던 그 악명 높은 첫 실행 끊김을 만들어 냈습니다. Impeller는 그 셰이더들을 빌드 시점에 미리 컴파일하므로, 애니메이션의 맨 첫 프레임이 100번째 프레임만큼 부드럽습니다. Impeller는 또한 최신 그래픽 API를 직접 겨냥합니다. iOS에서는 Metal, Android에서는 Vulkan을 사용하며, 더 오래된 Android 하드웨어를 위한 OpenGL 폴백을 제공합니다. [GitHub의 Impeller 엔진 문서](https://github.com/flutter/flutter/blob/master/docs/engine/impeller/README.md)는 렌더링 아키텍처와 플랫폼 지원 매트릭스를 상세히 설명합니다. | 측면 | Skia(레거시) | Impeller(2026 기본값) | |--------|---------------|-------------------------| | 셰이더 컴파일 | 런타임, 첫 실행 끊김을 유발 | 사전, 빌드 시점 | | iOS 백엔드 | OpenGL / Metal | Metal | | Android 백엔드 | OpenGL | Vulkan, OpenGL 폴백 | | 첫 애니메이션의 부드러움 | 이따금 끊김 | 일관됨 | 대부분의 프로젝트에서 마이그레이션은 공짜입니다. 최근 Flutter 릴리스는 Impeller를 자동으로 활성화하고, 옛 셰이더 워밍업 우회책(`--purge-persistent-cache`, 번들된 `.sksl` 파일)은 더 이상 필요 없어 삭제할 수 있습니다. Flutter가 지원하는 GLSL 하위 집합에 맞춰 작성된 사용자 지정 `FragmentProgram` 셰이더도 그대로 이어집니다. Impeller가 이를 프레임워크 자체 효과와 동일한 오프라인 파이프라인으로 컴파일하기 때문입니다. 앱이 어느 엔진에서 실행되는지 확인하는 데는 한 줄이면 됩니다. 디버그 빌드는 시작 시 활성 백엔드를 출력하고, DevTools 인스펙터는 앱의 렌더링 세부 정보에서 이를 보고합니다. Impeller가 확인되면, 남은 성능 작업은 위젯 계층으로 올라갑니다. 그곳에서 프레임워크는 화면이 실제로 바뀌는 것보다 훨씬 자주 리빌드합니다. 이 가이드의 나머지가 초점을 맞추는 곳이 바로 그곳입니다. 빠른 엔진도 초당 수백 번 스스로를 다시 그리는 위젯 트리는 구할 수 없기 때문입니다. ## const와 위젯 분할로 Flutter 리빌드 줄이기 모든 `setState`는 자신의 위젯을 오래된 것으로 표시하고 그 요소와 하위 트리에 대해 `build`를 다시 실행합니다. 요령은 그 하위 트리를 가능한 한 좁게 유지하는 것입니다. 두 가지 습관이 작업의 대부분을 담당합니다. 정적 위젯을 `const`로 표시하는 것, 그리고 상태를 아래로 밀어 상태를 가진 부분을 작게 유지하는 것입니다. `const` 위젯은 단일 인스턴스로 정규화되므로, 부모가 리빌드될 때 Flutter는 동일한 참조를 비교하고, 변화가 없다고 판단하여, 그 가지의 다시 그리기를 완전히 건너뜁니다. ```dart // product_screen.dart // A cart update repaints the badge, not the header. class ProductScreen extends StatefulWidget { const ProductScreen({super.key}); @override State createState() => _ProductScreenState(); } class _ProductScreenState extends State { int _cartCount = 0; @override Widget build(BuildContext context) { return Column( children: [ const ProductHeader(), // const: never rebuilds on setState CartBadge(count: _cartCount), // only this subtree rebuilds FilledButton( onPressed: () => setState(() => _cartCount++), child: const Text('Add to cart'), ), ], ); } } ``` `analysis_options.yaml`에서 `prefer_const_constructors` 린트를 활성화하면 이 규율이 컴파일러 검사로 바뀌므로, `const` 기회가 조용히 사라지는 일이 없습니다. 관련 규칙 `prefer_const_literals_to_create_immutables`는 위젯에 전달되는 리스트와 맵에도 같은 보장을 확장합니다. 위젯 키도 여기서 언급할 가치가 있습니다. 리스트의 순서가 바뀌거나 위젯의 타입은 같지만 정체성이 바뀔 때, `ValueKey`나 `ObjectKey`는 Flutter가 상태를 헐고 다시 만드는 대신 이전 요소를 올바른 새 위젯 인스턴스에 매칭하도록 해 줍니다. 키의 부재는 동적 리스트에서 스크롤 위치가 사라지고 애니메이션이 리셋되는 흔한 원인이며, 그로 인한 추가 리빌드 작업은 놓치기 쉽습니다. ## 바뀐 데이터로 리빌드 범위 좁히기 상태가 트리의 높은 곳에 있으면, 잘 분할된 레이아웃이라도 여전히 많이 리빌드됩니다. 해법은 값을 브로드캐스트하고 위젯 하나만 그것을 듣게 하는 것입니다. Flutter는 바로 이를 위해 `ValueNotifier`와 `ValueListenableBuilder`를 제공하며, 외부 패키지는 필요 없습니다. ```dart // article_list.dart // A scroll-to-top button that rebuilds alone, not the whole page. class ArticleList extends StatefulWidget { const ArticleList({super.key}); @override State createState() => _ArticleListState(); } class _ArticleListState extends State { final _showFab = ValueNotifier(false); final _controller = ScrollController(); @override void initState() { super.initState(); // Update the notifier, not setState: the list never rebuilds _controller.addListener(() => _showFab.value = _controller.offset > 400); } @override void dispose() { _showFab.dispose(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( controller: _controller, itemCount: 200, itemBuilder: (_, i) => ListTile(title: Text('Item $i')), ), floatingActionButton: ValueListenableBuilder( valueListenable: _showFab, builder: (_, visible, __) => visible ? FloatingActionButton( onPressed: () => _controller.jumpTo(0), child: const Icon(Icons.arrow_upward), ) : const SizedBox.shrink(), ), ); } } ``` 상태 관리 라이브러리는 같은 아이디어를 일반화합니다. Riverpod의 `ref.watch(provider.select(...))`와 Provider의 `Selector` 위젯은 모두 상태의 선택된 조각이 바뀔 때만 리빌드하며, 이것이 데이터가 많은 화면에서 부드러움과 버벅임을 가르는 결정적 요인입니다. 이 라이브러리들 사이의 절충은 [2026년 Flutter 상태 관리](/blog/flutter/flutter-state-management-2026-riverpod-bloc-getx) 가이드에서 다룹니다. > **리빌드 범위에 주의** > > 화면의 루트 근처에서 `setState`를 호출하거나 페이지 전체를 최상위 `Consumer`로 감싸면, 변경마다 조용히 수백 개의 위젯이 다시 그려집니다. 어떤 화면이 저렴하다고 단정하기 전에 DevTools의 리빌드 카운터로 프로파일하십시오. ## RepaintBoundary로 비싼 다시 그리기 격리하기 리빌드와 다시 그리기는 별개의 단계입니다. 위젯은 리빌드를 건너뛰고도, 소란스러운 이웃이 레이어를 공유하기 때문에 다시 그려질 수 있습니다. `RepaintBoundary`는 하위 트리에 자체 레이어를 부여하므로, 애니메이션되는 차트가 정적 카드에 매 프레임 다시 그리도록 강요할 수 없습니다. ```dart // dashboard.dart // The live chart repaints on its own layer, cards stay untouched. class Dashboard extends StatelessWidget { const Dashboard({required this.ticker, super.key}); final Stream ticker; @override Widget build(BuildContext context) { return Column( children: [ // SummaryCards is static, so isolate it from the animation below const RepaintBoundary(child: SummaryCards()), RepaintBoundary( child: StreamBuilder( stream: ticker, builder: (_, snap) => LivePriceChart(value: snap.data ?? 0), ), ), ], ); } } ``` RepaintBoundary는 공짜가 아닙니다. 각각이 레이어 하나를 할당하므로 여기저기 뿌리면 도움보다 해가 큽니다. 정말로 애니메이션되는 영역과 긴 스크롤 리스트 주위에 사용하고, 타임라인에서 이득을 확인하십시오. [RepaintBoundary API 레퍼런스](https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html)는 디버그 다시 그리기 오버레이가 실제로 다시 그려지는 레이어를 어떻게 강조하는지 문서화합니다. ## build 메서드를 저렴하고 부작용 없이 유지하기 `build` 메서드는 리빌드마다 실행되므로, 그 안의 비싼 것은 무엇이든 배가됩니다. 규칙은 `build`가 상태를 읽고 위젯을 반환하는 것, 그 이상은 아니라는 것입니다. `build` 안에서 `ScrollController`, `AnimationController`, `Future`를 할당하면 매 프레임 다시 생성되고 이전 인스턴스가 누출됩니다. 이 객체들은 `initState`나 `dispose`에서 해제되는 `late final` 필드에 속합니다. 무거운 동기 작업이 두 번째 함정입니다. `build` 안에서 JSON을 파싱하거나, 큰 리스트를 필터링하거나, 날짜를 포맷하면, 기저 데이터가 바뀌지 않았어도 그 작업이 매 다시 그리기마다 재실행됩니다. 입력이 바뀔 때 결과를 한 번만 계산해 캐시하거나, 프레임워크가 한 프레임을 막고 기다리지 않도록 `FutureBuilder`로 옮기십시오. 다른 값에서 파생되는 값은 의존성이 바뀔 때만 재계산하는 게터 뒤에 메모이제이션하십시오. `Opacity` 위젯은 이름을 밝힐 만한 구체적인 원흉입니다. 하위 트리를 `Opacity`로 감싸면 화면 밖 `saveLayer`가 강제되는데, 이는 파이프라인에서 가장 비싼 연산 중 하나입니다. 페이드 애니메이션에는 `AnimatedOpacity`나 `FadeTransition`이 더 저렴하고, 고정된 색조에는 `foregroundDecoration`이나 셰이더를 통해 페인트 수준에서 색을 적용하면 레이어를 완전히 피할 수 있습니다. 같은 주의가 큰 표면에서 안티에일리어싱을 켠 `ClipPath`와 `ClipRRect`에도 적용되며, 이들 역시 뒤에서 `saveLayer`를 유발합니다. 마지막으로, `const` 컬렉션과 매 빌드마다 새 클로저를 포획하지 않는 콜백을 선호하십시오. 기본값으로 전달한 `const []`와 인라인 람다 대신의 메서드 참조는 모두 위젯 동등성 검사를 저렴하게 유지하며, 이는 앞서 설명한 `const` 단락(短絡)이 실제로 발동하도록 해 줍니다. ## 긴 리스트와 이미지를 효율적으로 구성하기 긴 피드와 이미지 그리드는 순진한 Flutter 코드가 프레임을 떨어뜨리는 지점입니다. 두 가지 규칙이 대부분의 경우를 다룹니다. 행을 지연 구성하는 것, 그리고 이미지를 소스 해상도가 아니라 표시되는 크기로 디코딩하는 것입니다. ```dart // feed.dart // Lazy rows plus right-sized image decoding keep scrolling at 60fps. class Feed extends StatelessWidget { const Feed({required this.posts, super.key}); final List posts; @override Widget build(BuildContext context) { return ListView.builder( itemCount: posts.length, // build on demand, not all at once cacheExtent: 600, // pre-build past the viewport edge itemBuilder: (context, index) { final post = posts[index]; return ListTile( leading: Image.network( post.thumbnailUrl, width: 48, height: 48, cacheWidth: 96, // decode at display size, save memory ), title: Text(post.title), ); }, ); } } ``` 3000픽셀 전체 해상도 이미지를 48픽셀 아바타에 욱여넣으면, 그 하나하나마다 메모리와 디코딩 시간을 낭비합니다. `cacheWidth`를 설정하면 엔진에 목표 크기로 디코딩하라고 지시하게 되며, 이것만으로도 리스트의 메모리 사용량을 한 자릿수 규모로 줄일 수 있습니다. Flutter의 [성능 모범 사례](https://docs.flutter.dev/perf/best-practices)는 같은 원칙을 따르는 `Opacity`, `saveLayer`, 클립 사용에 대한 추가 지침을 모아 둡니다. ## 2026년 Flutter 최적화를 DevTools로 측정하기 위의 모든 규칙은 가정이 아니라 검증되어야 합니다. Flutter DevTools가 진실의 원천입니다. 성능 오버레이(디버그 콘솔의 `P` 또는 `flutter run --profile`)는 UI와 래스터라는 두 그래프를 그리며, 초록 선을 넘는 막대는 모두 떨어진 프레임입니다. 이어서 타임라인 뷰가 각 느린 프레임을 특정 `build`, 레이아웃, 페인트 호출에 귀속시킵니다. 항상 실제 기기에서 `--profile` 모드로 프로파일하십시오. 디버그 빌드는 최적화되지 않은 Dart를 실행하고 모든 측정치를 부풀리므로, 디버그에서 버벅이는 화면이 컴파일 후에는 완벽하게 도는 경우가 많습니다. [DevTools 성능 가이드](https://docs.flutter.dev/tools/devtools/performance)는 프레임 차트를 읽고 버벅임의 원인을 찾아내는 과정을 짚어 주며, 같은 프로파일링 근육은 [Flutter 애니메이션과 렌더링](/technologies/flutter/interview-questions/animations)에 관한 면접에서도 등장합니다. > **빠른 프로파일링 루프** > > 프로파일 모드로 실행하고, 버벅이는 상호작용을 재현하고, 타임라인을 열어 프레임을 지속 시간으로 정렬하십시오. 최악의 프레임은 거의 언제나 지나치게 큰 단일 리빌드나 캐시되지 않은 이미지를 가리키며, 이는 추측하는 것보다 훨씬 빠릅니다. 더 넓은 [Flutter 기술 허브](/technologies/flutter)는 프로덕션 성능 전략을 완성하는 상태 관리, 테스트, 렌더링 주제를 연결합니다. ## 결론 2026년 Flutter 성능은 미세한 요령보다는, 일관되게 적용하는 짧은 습관 목록에 관한 것입니다. - Impeller 위에서 배포하고 오래된 셰이더 워밍업 코드를 삭제하십시오. 첫 프레임 버벅임 문제는 엔진 수준에서 해결되었습니다. - 정적 위젯을 `const`로 표시하고 `prefer_const_constructors` 린트로 강제하십시오. - 상태를 가진 위젯을 작게 분할해, 각 `setState`가 가능한 한 좁은 하위 트리를 다시 그리게 하십시오. - 변하는 값을 화면 전체를 리빌드하는 대신 `ValueListenableBuilder`나 `select` 스타일 API로 브로드캐스트하십시오. - 정말로 애니메이션되는 영역을 `RepaintBoundary`로 감싸되, 더 추가하기 전에 측정하십시오. - 리스트는 `ListView.builder`로 구성하고 이미지는 `cacheWidth`로 표시 크기에 맞춰 디코딩하십시오. - 실제 하드웨어에서 `--profile` 모드로 프로파일하고, 다음에 무엇을 최적화할지 직감이 아니라 DevTools가 결정하게 하십시오. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/flutter/flutter-performance-optimization-2026-impeller-rebuilds