
화면이 자꾸 깜빡거려요 (Rebuild의 함정)
상태만 바꿨는데 왜 이미지까지 다시 로딩될까요? FutureBuilder를 build 안에 넣는 실수부터 const 생성자, RepaintBoundary까지, 플러터 앱이 번개처럼 파닥거리는 원인을 잡습니다.

상태만 바꿨는데 왜 이미지까지 다시 로딩될까요? FutureBuilder를 build 안에 넣는 실수부터 const 생성자, RepaintBoundary까지, 플러터 앱이 번개처럼 파닥거리는 원인을 잡습니다.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

API에서 데이터를 가져와서 리스트를 보여주는 간단한 앱이었습니다. 그런데 리스트의 '좋아요' 하트 버튼만 누르면, 전체 화면이 미세하게 번쩍하고 깜빡(Flicker)이는 겁니다. 심지어 리스트에 이미지가 있다면, 이미지가 하얗게 변했다가 다시 로딩되는 끔찍한 현상까지 발생했습니다.
"나는 하트 아이콘 하나 바꿨을 뿐인데, 왜 온 우주(화면 전체)가 다시 그려지는가?"
이것은 Flutter의 Rebuild 메커니즘을 오해하고 저지른 저의 실수였습니다.
FutureBuilder가장 흔한 실수 1순위입니다. FutureBuilder를 쓸 때 future 파라미터에 비동기 함수를 직접 넣는 경우입니다.
class MyPage extends StatefulWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
// 😱 build 될 때마다 getImages()가 다시 실행됨!
future: getImages(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // 로딩 스피너
}
return ListView(...);
},
);
}
}
생각해보세요.
setState() 호출.setState는 "화면을 다시 그려라(Rebuild)"는 명령임.build() 메서드가 다시 실행됨.getImages()가 다시 호출됨.FutureBuilder는 새로운 Future를 받았으니, 상태를 다시 waiting으로 초기화함.Future 객체는 initState에서 딱 한 번만 만들어야 합니다.
class _MyPageState extends State<MyPage> {
// 1. Future를 저장할 변수
late Future<List<Image>> _imagesFuture;
@override
void initState() {
super.initState();
// 2. 초기화할 때 딱 한 번만 API 호출
_imagesFuture = getImages();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _imagesFuture, // 3. 저장해둔 변수 사용
builder: ...,
);
}
}
이제 setState를 백 번 호출해서 build()가 다시 실행되더라도, _imagesFuture는 이미 완료된(Completed) 상태의 객체 그대로입니다.
FutureBuilder는 다시 로딩 상태로 돌아가지 않고, 즉시 데이터를 보여줍니다. 깜빡임이 사라집니다.
데이터 로딩 문제가 아닌데도 깜빡인다면, 불필요한 위젯 렌더링 때문일 수 있습니다.
Flutter 성능 최적화의 알파이자 오메가는 const입니다.
// ❌ 나쁜 예
Column(
children: [
Text("변하지 않는 제목"), // 매번 새로 생성됨
MyDynamicWidget(),
],
)
// ✅ 좋은 예
Column(
children: [
const Text("변하지 않는 제목"), // 컴파일 타임 상수 (재사용됨)
MyDynamicWidget(),
],
)
const를 붙이면 Flutter 엔진에게 "이 위젯은 부모가 리빌드돼도 절대 안 변합니다. 그냥 재사용하세요."라고 각서를 써주는 셈입니다.
Rebuild 범위를 획기적으로 줄일 수 있습니다.
PageView나 TabBarView를 쓸 때, 탭을 이동했다가 돌아오면 스크롤 위치가 초기화되거나 데이터가 다시 로딩되는 현상을 겪습니다.
이는 탭이 화면에서 사라지면 위젯이 파괴(Dispose)되기 때문입니다.
이걸 막으려면 AutomaticKeepAliveClientMixin을 써야 합니다.
class MyTab extends StatefulWidget { ... }
// 1. Mixin 추가
class _MyTabState extends State<MyTab> with AutomaticKeepAliveClientMixin {
// 2. true 반환
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 3. super.build 호출 필수!
return ListView(...);
}
}
이제 이 탭은 다른 탭으로 갔다가 돌아와도, 메모리에서 파괴되지 않고 살아있습니다. 스크롤 위치도 그대로 유지됩니다.
리스트 안에 복잡한 그림이나 애니메이션이 있는 경우, 리스트를 스크롤할 때마다 버벅일 수 있습니다.
이럴 때 해당 위젯을 RepaintBoundary로 감싸주면 효과가 좋습니다.
RepaintBoundary(
child: ComplexGraphWidget(),
)
이건 "이 부분은 독립적으로 그림(Paint)을 그릴 테니, 주변 위젯이 다시 그려져도 나 건드리지 마"라고 격리하는 겁니다. GPU 부하를 줄여줍니다.
"도대체 어디가 리빌드되는 거야?" 눈으로 확인하고 싶다면 Flutter Performance Overlay를 켜보세요.
Flutter: Toggle Performance Overlay 검색 & 실행더 확실한 건 "Highlight Repaints" 옵션입니다. DevTools에서 이 옵션을 켜면, 리빌드되는 위젯에 무지개색 테두리가 생깁니다. 가만히 있는데 테두리가 번쩍거린다? 100% 최적화 대상입니다.
수천 개의 메시지가 있는 채팅방을 구현했습니다. 스크롤을 위로 휙 올리면(Fling), 앱이 뚝뚝 끊깁니다.
IntrinsicHeight 위젯을 남발함. (이건 레이아웃 계산을 O(N^2)로 만듦).ListView의 cacheExtent를 늘려서, 화면 밖의 위젯을 미리 좀 그려놓음.const로 분리.결과적으로 60fps를 방어했습니다. Flutter 성능의 8할은 "불필요한 연산 줄이기"입니다.
User.fromJson이 너무 느려요 (Isolate)가끔 위젯 문제가 아니라, 데이터 파싱(JSON Parsing)이 UI 스레드를 막아서 깜빡이는 경우도 있습니다.
데이터가 방대하다면(예: 10MB JSON), compute 함수를 써서 백그라운드 스레드(Isolate)로 넘기세요.
// UI 스레드 안 멈춤!
final events = await compute(parseEvents, jsonString);
애니메이션이 끊긴다면 "무거운 작업"이 범인입니다.
"깜빡임"은 사용자가 앱을 "싸구려"라고 느끼게 만드는 가장 큰 요인입니다.
Future는 build 안에 넣지 마라. initState로 빼라.const를 습관화하라. VS Code에서 'Prefer const' 린트를 켜라.AutomaticKeepAliveClientMixin을 써라.
CachedNetworkImage를 써라. (기본 Image.network는 캐싱 기능이 약함)RepaintBoundary의 양면성 한 걸음 더"그럼 모든 위젯에 RepaintBoundary를 씌우면 짱 빠르겠네요?"
아니요. 메모리를 더 씁니다.
별도의 레이어(Layer)를 생성해서 이미지처럼 저장해두는 방식(Raster Cache)이라서, VRAM을 잡아먹습니다.
20% 이상을 차지하는 복잡한 위젯CustomPaint, VideoPlayer, GoogleMapText, Iconconst 생성자로 프레임 드랍 잡기복잡한 대시보드 화면이 있었습니다. 데이터 갱신(Stream)이 1초에 60번 일어나는데, 그때마다 전체 화면이 버벅였습니다.
Before:return Column(
children: [
HeaderWidget(), // ❌ const 아님 -> 매번 새로 생성
GraphWidget(data: streamData),
FooterWidget(), // ❌ const 아님 -> 매번 새로 생성
],
);
After:
return Column(
children: [
const HeaderWidget(), // ✅ 재사용 (리빌드 스킵)
GraphWidget(data: streamData),
const FooterWidget(), // ✅ 재사용 (리빌드 스킵)
],
);
이 간단한 변화로 build 시간이 16ms -> 2ms로 줄었습니다.
Flutter가 "어? 이건 아까 그거랑 똑같네?" 하고 Diffing을 건너뛰기 때문입니다.
투명도를 줄 때 Opacity 위젯을 많이 쓰시죠?
이 녀석은 성능의 주적입니다.
자식 위젯을 버퍼에 비트맵으로 그리고, 투명도를 적용해서 다시 그리는(Compositing) 과정을 거칩니다.
단순히 애니메이션을 하고 싶다면 FadeTransition이 훨씬 가볍습니다.
고정된 투명도라면 Color.withOpacity나 Container(color: ...)를 우선 고려하세요.
Opacity는 최후의 수단입니다.
"깜빡임"은 사용자가 앱을 "싸구려"라고 느끼게 만드는 가장 큰 요인입니다.
Future는 build 안에 넣지 마라. initState로 빼라.const를 습관화하라. VS Code에서 'Prefer const' 린트를 켜라.AutomaticKeepAliveClientMixin을 써라.
CachedNetworkImage를 써라. (기본 Image.network는 캐싱 기능이 약함)