Flutter 튜토리얼 52편: 렌더링 성능 프로파일링
Flutter 렌더링 성능 프로파일링
Flutter 앱이 부드럽게 동작하려면 초당 60프레임(60fps)을 유지해야 합니다. 이 튜토리얼에서는 렌더링 성능을 측정하고 최적화하는 방법을 배웁니다.
학습 목표
- 60fps의 의미와 중요성 이해하기
- Performance Overlay 활용하기
- DevTools Performance View로 병목 분석하기
- 일반적인 성능 문제 해결하기
1. 60fps와 프레임 예산
Why: 왜 60fps가 중요한가요?
인간의 눈은 초당 60프레임 이상에서 움직임을 자연스럽게 인식합니다. 프레임 속도가 떨어지면 사용자는 앱이 “버벅거린다”고 느끼게 됩니다. 이는 사용자 경험을 크게 저하시킵니다.
What: 프레임 예산의 개념
60fps를 달성하려면 각 프레임을 16밀리초(ms) 안에 완성해야 합니다.
| 프레임 속도 | 프레임당 시간 | 사용자 경험 |
|---|---|---|
| 60fps | ~16ms | 매우 부드러움 |
| 30fps | ~33ms | 약간 버벅임 |
| 15fps | ~66ms | 심하게 버벅임 |
What: UI 스레드와 Raster 스레드
Flutter는 두 개의 주요 스레드1를 사용합니다:
| 스레드 | 역할 | 병목 시 증상 |
|---|---|---|
| UI 스레드 | 위젯 빌드, 레이아웃 | 애니메이션 끊김 |
| Raster 스레드 | 화면 렌더링 | 화면 깜빡임 |
How: 프로파일 모드 사용
성능을 정확히 측정하려면 프로파일 모드2로 실행해야 합니다.
# 명령줄에서 프로파일 모드 실행flutter run --profileVS Code에서는 launch.json에 다음 설정을 추가합니다:
{ "configurations": [ { "name": "Flutter (Profile)", "request": "launch", "type": "dart", "flutterMode": "profile" } ]}Watch out: 주의사항
디버그 모드에서는 성능이 정확하지 않습니다. JIT 컴파일과 디버그 검사로 인해 실제보다 느리게 동작합니다. 항상 프로파일 모드나 릴리스 모드에서 성능을 측정하세요.
2. Performance Overlay 사용하기
Why: Performance Overlay가 필요한 이유
Performance Overlay3는 앱 실행 중 실시간으로 성능을 확인할 수 있는 도구입니다. 코드를 수정하지 않고도 바로 성능 문제를 발견할 수 있습니다.
What: 그래프 읽는 방법
Performance Overlay는 두 개의 그래프를 표시합니다:
| 그래프 색상 | 의미 |
|---|---|
| 녹색 막대 | 16ms 이하 (정상) |
| 빨간색 막대 | 16ms 초과 (문제) |
| 빨간색 점선 | 16ms 기준선 |
How: Performance Overlay 활성화
코드로 활성화
import 'package:flutter/material.dart';
void main() { runApp( MaterialApp( showPerformanceOverlay: true, // 여기에 추가 home: const MyHomePage(), ), );}DevTools에서 활성화
flutter run --profile로 앱을 실행합니다- DevTools를 열고 Performance 탭으로 이동합니다
- “Performance Overlay” 버튼을 클릭합니다
How: 문제 진단하기
// 나쁜 예: UI 스레드에서 무거운 작업class BadExample extends StatelessWidget { @override Widget build(BuildContext context) { // 이 작업이 16ms를 초과하면 프레임 드롭 발생 final result = expensiveCalculation(); return Text(result); }}
// 좋은 예: 미리 계산하거나 비동기로 처리class GoodExample extends StatefulWidget { @override State<GoodExample> createState() => _GoodExampleState();}
class _GoodExampleState extends State<GoodExample> { String? _result;
@override void initState() { super.initState(); _loadData(); }
Future<void> _loadData() async { final result = await compute(expensiveCalculation, null); setState(() => _result = result); }
@override Widget build(BuildContext context) { return _result != null ? Text(_result!) : const CircularProgressIndicator(); }}Watch out: 주의사항
Performance Overlay는 프로파일 모드에서만 정확합니다. 또한 오버레이 자체도 약간의 성능 오버헤드가 있으므로, 최종 측정은 오버레이 없이 진행하는 것이 좋습니다.
3. DevTools Performance View
Why: 더 상세한 분석이 필요한 이유
Performance Overlay는 문제가 있는지 여부만 알려줍니다. 정확히 어떤 코드가 문제인지 알려면 DevTools의 Performance View가 필요합니다.
What: Performance View의 구성
How: Performance View 사용하기
- 프로파일 모드로 앱을 실행합니다
- DevTools를 엽니다 (터미널에 표시된 URL 클릭)
- Performance 탭을 선택합니다
- “Record” 버튼을 클릭하여 녹화를 시작합니다
- 앱에서 성능 문제가 발생하는 동작을 수행합니다
- “Stop” 버튼을 클릭합니다
How: Frame Chart 분석
Frame Chart에서 각 프레임의 처리 시간을 확인할 수 있습니다:
| 색상 | 의미 | 조치 |
|---|---|---|
| 파란색 | UI 스레드 작업 | 위젯 빌드 최적화 필요 |
| 초록색 | Raster 스레드 작업 | 렌더링 최적화 필요 |
| 빨간색 프레임 | 16ms 초과 | 즉시 조사 필요 |
How: Timeline Events 분석
특정 프레임을 클릭하면 상세 타임라인을 볼 수 있습니다:
// 타임라인에 커스텀 이벤트 추가하기import 'dart:developer';
void myFunction() { Timeline.startSync('myFunction'); try { // 측정하고 싶은 코드 doSomething(); } finally { Timeline.finishSync(); }}Watch out: 주의사항
DevTools는 많은 데이터를 수집하므로, 너무 오래 녹화하면 분석이 어려워집니다. 문제가 발생하는 구간만 짧게 녹화하는 것이 좋습니다.
4. 일반적인 렌더링 문제
Why: 자주 발생하는 문제를 알아야 하는 이유
대부분의 성능 문제는 몇 가지 패턴으로 귀결됩니다. 이러한 패턴을 알고 있으면 빠르게 문제를 해결할 수 있습니다.
What: saveLayer의 과도한 사용
saveLayer4는 비용이 큰 연산입니다.
일부 위젯은 내부적으로 saveLayer를 호출합니다.
How: Opacity 최적화
// 나쁜 예: Opacity 위젯 사용Opacity( opacity: 0.5, child: Container( color: Colors.red, width: 100, height: 100, ),)
// 좋은 예: 색상에 직접 투명도 적용Container( color: Colors.red.withOpacity(0.5), width: 100, height: 100,)What: 불필요한 위젯 리빌드
위젯이 너무 자주 리빌드되면 성능이 저하됩니다.
// 나쁜 예: 전체 위젯 리빌드class BadCounter extends StatefulWidget { @override State<BadCounter> createState() => _BadCounterState();}
class _BadCounterState extends State<BadCounter> { int count = 0;
@override Widget build(BuildContext context) { return Column( children: [ // 이 비싼 위젯도 매번 리빌드됨 const ExpensiveWidget(), Text('Count: $count'), ElevatedButton( onPressed: () => setState(() => count++), child: const Text('Increment'), ), ], ); }}
// 좋은 예: 필요한 부분만 리빌드class GoodCounter extends StatefulWidget { @override State<GoodCounter> createState() => _GoodCounterState();}
class _GoodCounterState extends State<GoodCounter> { int count = 0;
@override Widget build(BuildContext context) { return Column( children: [ // const로 선언하여 리빌드 방지 const ExpensiveWidget(), // 또는 별도 위젯으로 분리 CounterText(count: count), ElevatedButton( onPressed: () => setState(() => count++), child: const Text('Increment'), ), ], ); }}How: RepaintBoundary 사용
RepaintBoundary5로 리페인트 영역을 제한할 수 있습니다.
class OptimizedList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemCount: 1000, itemBuilder: (context, index) { return RepaintBoundary( child: ComplexListItem(index: index), ); }, ); }}What: 캐시되지 않는 이미지
동일한 이미지를 반복해서 디코딩하면 성능이 저하됩니다.
// 이미지 캐싱 확인class ImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Image.asset( 'assets/image.png', cacheWidth: 200, // 캐시 크기 지정 cacheHeight: 200, ); }}Watch out: 주의사항
RepaintBoundary를 과도하게 사용하면 오히려 메모리가 증가합니다. 성능 문제가 확인된 곳에만 선택적으로 사용하세요.
5. 위젯 리빌드 프로파일러
Why: 리빌드를 추적해야 하는 이유
불필요한 위젯 리빌드는 성능 저하의 주요 원인입니다. 어떤 위젯이 언제 리빌드되는지 추적하면 최적화 포인트를 찾을 수 있습니다.
What: Widget Rebuild Stats
DevTools의 Performance 탭에서 “Track Widget Builds”를 활성화하면 위젯별 빌드 횟수를 확인할 수 있습니다.
How: 리빌드 추적 활성화
// main.dart에서 디버그 플래그 활성화import 'package:flutter/rendering.dart';
void main() { // 리빌드 추적 (프로파일/디버그 모드에서만) debugProfileBuildsEnabled = true;
runApp(const MyApp());}How: 불필요한 리빌드 방지
// 1. const 생성자 활용const MyWidget(); // 리빌드 방지
// 2. shouldRebuild 구현 (InheritedWidget)class MyInheritedWidget extends InheritedWidget { final int data;
const MyInheritedWidget({ required this.data, required super.child, });
@override bool updateShouldNotify(MyInheritedWidget oldWidget) { return data != oldWidget.data; // 데이터가 변경된 경우만 알림 }}
// 3. 선택적 리빌드 (Consumer, Selector)// Provider 패키지 사용 예시Consumer<CounterModel>( builder: (context, counter, child) { return Text('${counter.count}'); },)Watch out: 주의사항
debugProfileBuildsEnabled는 성능에 영향을 주므로
배포 빌드에서는 반드시 제거해야 합니다.
6. 성능 최적화 체크리스트
측정 및 분석
- 프로파일 모드로 실행
- Performance Overlay로 프레임 드롭 확인
- DevTools Performance View로 상세 분석
일반적인 최적화
- Opacity 대신 색상 투명도 사용
- const 생성자 활용
- 무거운 작업은 Isolate로 분리
- ListView.builder 사용 (긴 리스트)
고급 최적화
- RepaintBoundary 적절히 사용
- saveLayer 호출 최소화
- 이미지 캐싱 설정
- 위젯 리빌드 최적화
마무리
이번 튜토리얼에서는 Flutter 앱의 렌더링 성능을 측정하고 최적화하는 방법을 배웠습니다.
핵심 정리
| 주제 | 핵심 내용 |
|---|---|
| 프레임 예산 | 16ms 이내에 프레임 완성 (60fps) |
| 스레드 구조 | UI 스레드 + Raster 스레드 |
| 측정 도구 | Performance Overlay, DevTools |
| 주요 문제 | saveLayer, 불필요한 리빌드, 캐시 미사용 |
다음 단계
- Flutter 튜토리얼 53편: Isolate와 동시성에서 무거운 작업을 백그라운드로 분리하는 방법을 배워보세요.
참고 자료
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!