Flutter 앱 성능 최적화 실전 가이드
Flutter 앱은 불필요한 리빌드, UI 스레드에서의 무거운 연산, 비효율적인 리스트 처리로 인해 끊김 현상이 자주 발생합니다. 이 글에서는 주요 실수와 해결 방법을 단계별로 설명합니다: 업데이트 범위 최소화부터 빌더와 애니메이션의 올바른 사용법까지. 이러한 기법들은 저사양 기기에서도 60 FPS를 유지하는 데 도움이 됩니다.
불필요한 UI 리빌드 최소화
불필요한 리빌드는 성능 저하의 주요 원인입니다. setState()는 StatefulWidget의 전체 서브트리를 리빌드하는데, 여기에는 무거운 리스트와 이미지도 포함됩니다. 이는 레이아웃 재계산과 FPS 하락으로 이어집니다.
문제가 있는 코드:
class MyScreen extends StatefulWidget {
@override
State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $counter'),
ElevatedButton(
onPressed: () {
setState(() {
counter++; // 전체 트리를 리빌드합니다
});
},
child: Text('Increment'),
),
],
);
}
}
해결책: ValueListenableBuilder는 UI의 의존적인 부분만 업데이트합니다.
class MyScreen extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (_, value, __) {
return Text('Counter: $value');
},
),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
],
);
}
}
마찬가지로 StreamBuilder, provider의 Selector, BlocBuilder도 동일한 원리로 작동합니다. 리페인트 영역이 최소화되어 CPU 부하가 줄어듭니다.
const 생성자 사용하기
const 없이 리빌드할 때마다 새로운 위젯 객체가 생성되면 GC와 CPU에 부담이 됩니다. 수백 개의 아이콘, 텍스트, 패딩이 있는 리스트에서는 특히 중요합니다.
최적의 접근법:
Widget build(BuildContext context) {
return const Column(
children: [
Text('Hello'),
Icon(Icons.home),
Padding(
padding: EdgeInsets.all(8),
child: Text('World'),
),
],
);
}
Flutter는 const 객체를 재사용하여 할당을 줄입니다. 수천 개의 작은 위젯으로 구성된 복잡한 UI에서 효과가 두드러집니다.
build() 외부로 로직 이동하기
build() 내에서 정렬, 필터링, 파싱을 수행하면 매번 호출될 때마다 실행됩니다—초당 최대 60번까지. 60 FPS를 유지하려면 프레임당 16ms만 사용 가능하므로 무거운 연산은 끊김을 유발합니다.
올바른 접근법:
late List<int> filtered;
@override
void initState() {
super.initState();
final sorted = [...items]..sort((a, b) => a.compareTo(b));
filtered = sorted.where((e) => e > 10).toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) => Text('${filtered[i]}'),
);
}
로직은 initState나 비즈니스 레이어에서 한 번 실행됩니다. build()는 선언적으로 유지됩니다.
ListView.builder로 지연 리스트 구현하기
children: [...]를 사용한 ListView는 보이지 않는 항목까지 한 번에 모두 생성합니다. 100개 이상의 항목이 있으면 메모리와 초기 렌더링에 부담이 됩니다.
권장사항:
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
);
},
);
}
스크롤할 때 항목이 생성되며, 메모리가 최적화되고 재활용이 효율적으로 작동합니다.
리스트 안정성을 위한 키 사용하기
키 없이 Flutter는 리스트 업데이트 시 요소를 매칭할 수 없어 불필요한 리빌드와 시각적 오류를 유발합니다.
다음 경우에 중요합니다:
- 요소 재정렬
- 삭제/삽입
- 전환 애니메이션
ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
key: ValueKey(item.id),
title: Text(item.title),
);
},
);
ValueKey(item.id)는 올바른 업데이트를 보장합니다.
setState 없이 애니메이션 구현하기
Timer.periodic과 setState를 16–100ms마다 사용하면 UI 스레드에 과부하가 걸립니다.
표준 도구:
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (_, __) {
return LinearProgressIndicator(value: controller.value);
},
);
}
AnimationController는 기기의 FPS와 동기화하여 최소한의 UI만 업데이트합니다.
작은 위젯으로 분리하기
하나의 거대한 StatefulWidget은 어떤 변경이든 전체를 리빌드합니다.
구성의 장점:
- 지역화된 리빌드
- 코드 재사용
- 가독성과 테스트 용이성 향상
Widget build(BuildContext context) {
return Column(
children: const [
Header(),
Expanded(child: ItemsList()),
Footer(),
],
);
}
class Header extends StatelessWidget {
const Header();
@override
Widget build(BuildContext context) {
return Text('Header');
}
}
build() 외부에서 Future 처리하기
build() 내에서 future: fetchData()를 사용한 FutureBuilder는 매 리빌드마다 요청을 시작합니다.
late Future dataFuture;
@override
void initState() {
super.initState();
dataFuture = fetchData();
}
Widget build(BuildContext context) {
return FutureBuilder(
future: dataFuture,
builder: (_, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return Text(snapshot.data.toString());
},
);
}
Future는 한 번만 생성되며, UI는 변경에 반응합니다.
핵심 요약
- 업데이트 지역화: 전역 setState 대신 ValueListenableBuilder, AnimatedBuilder 사용
- const 활용: 복잡한 UI에서 할당을 50–70% 줄임
- initState에서 로직 처리: build()는 선언적 설명만 담당
- ListView.builder + 키: 안정적인 동적 리스트 구현
- 위젯 구성: 단일 거대 위젯보다 작은 StatelessWidgets 사용
— Editorial Team
아직 댓글이 없습니다.