📋 목차
1. WidgetsBinding이란?
1.1 정의
WidgetsBinding은 Flutter 프레임워크의 핵심 클래스로, Flutter 위젯 레이어와 Flutter 엔진 사이의 접착제(glue) 역할을 합니다.
// Singleton 패턴으로 구현됨
WidgetsBinding.instance // 앱 전체에서 단 하나의 인스턴스
1.2 주요 역할
- 프레임 스케줄링: 언제 위젯을 다시 그릴지 결정
- 생명주기 관리: 앱의 생명주기 이벤트 처리
- 입력 처리: 터치, 키보드 입력 등을 위젯에 전달
- 플랫폼 통신: 네이티브 플랫폼과의 메시지 전달
1.3 계층 구조
Object
└─ BindingBase
└─ GestureBinding
└─ SchedulerBinding
└─ ServicesBinding
└─ PaintingBinding
└─ SemanticsBinding
└─ RendererBinding
└─ WidgetsBinding ← 여기!
각 바인딩의 역할:
GestureBinding: 제스처 인식 및 히트 테스트SchedulerBinding: 프레임 스케줄링 및 콜백 관리ServicesBinding: 플랫폼 채널 및 시스템 서비스PaintingBinding: 이미지 캐시 및 셰이더 관리SemanticsBinding: 접근성 트리 관리RendererBinding: 렌더 트리 관리WidgetsBinding: 위젯 트리 관리 (최상위)
2. 핵심 개념
2.1 프레임(Frame)이란?
Flutter는 초당 60프레임(60fps) 또는 120프레임(120fps)으로 화면을 갱신합니다.
1 프레임 = 약 16.67ms (60fps 기준)
Frame Lifecycle:
┌─────────────────────────────────────┐
│ 1. Begin Frame │
│ - scheduleFrame() 호출됨 │
├─────────────────────────────────────┤
│ 2. Transient Frame Callbacks │
│ - addPersistentFrameCallback() │
├─────────────────────────────────────┤
│ 3. Build Phase │
│ - build() 메서드 실행 │
├─────────────────────────────────────┤
│ 4. Layout Phase │
│ - 위젯 크기 및 위치 계산 │
├─────────────────────────────────────┤
│ 5. Paint Phase │
│ - 실제 픽셀 그리기 │
├─────────────────────────────────────┤
│ 6. End Frame │
│ - addPostFrameCallback() 실행 ◄─ 여기!
└─────────────────────────────────────┘
2.2 콜백(Callback) 타이밍
// 타이밍 다이어그램
Time ─────────────────────────────────────▶
Frame N-1 Frame N Frame N+1
│ │ │
│ ┌────┴────┐ │
│ │ build() │ │
│ └────┬────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ layout()│ │
│ └────┬────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ paint() │ │
│ └────┬────┘ │
│ │ │
│ ▼ │
│ postFrameCallback │
│ │ │
└─────────┼─────────────┘
│
다음 프레임 시작
3. 주요 메서드 상세 가이드
3.1 addPostFrameCallback()
개요
가장 많이 사용되는 메서드로, 현재 프레임의 렌더링이 완료된 직후에 한 번만 실행됩니다.
시그니처
void addPostFrameCallback(FrameCallback callback)
typedef FrameCallback = void Function(Duration timeStamp)
사용 시나리오
시나리오 1: Riverpod Provider 호출 (생명주기 오류 방지)
class _MyPageState extends ConsumerState<MyPage> {
@override
void initState() {
super.initState();
// ✅ 올바른 패턴: postFrameCallback 사용
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// 위젯 트리 빌드가 완료된 후 실행
ref.read(myProvider.notifier).loadData();
}
});
// ❌ 잘못된 패턴: 직접 호출
// ref.read(myProvider.notifier).loadData();
// → "Tried to modify a provider while widget tree was building" 오류
}
}
시나리오 2: 위젯 크기 측정
class MeasureWidget extends StatefulWidget {
@override
State<MeasureWidget> createState() => _MeasureWidgetState();
}
class _MeasureWidgetState extends State<MeasureWidget> {
final GlobalKey _key = GlobalKey();
Size? _widgetSize;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// ✅ 위젯이 완전히 렌더링된 후 크기 측정
final RenderBox box = _key.currentContext!.findRenderObject() as RenderBox;
setState(() {
_widgetSize = box.size;
});
print('위젯 크기: ${_widgetSize!.width} x ${_widgetSize!.height}');
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
key: _key,
width: 200,
height: 100,
color: Colors.blue,
child: const Text('측정할 위젯'),
),
if (_widgetSize != null)
Text('크기: ${_widgetSize!.width} x ${_widgetSize!.height}'),
],
);
}
}
시나리오 3: 스크롤 위치 자동 이동
class AutoScrollPage extends StatefulWidget {
@override
State<AutoScrollPage> createState() => _AutoScrollPageState();
}
class _AutoScrollPageState extends State<AutoScrollPage> {
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// ✅ 리스트가 완전히 빌드된 후 스크롤
_scrollController.animateTo(
500.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
);
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: 100,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
);
}
}
주의사항
// ❌ 잘못된 사용: mounted 체크 없음
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {}); // 위젯이 dispose되었을 수 있음!
});
// ✅ 올바른 사용: mounted 체크
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
3.2 addPersistentFrameCallback()
개요
매 프레임마다 영구적으로 실행되는 콜백을 등록합니다. 제거할 수 없으므로 주의해서 사용해야 합니다.
시그니처
void addPersistentFrameCallback(FrameCallback callback)
사용 시나리오
시나리오 1: 커스텀 애니메이션
class CustomAnimationWidget extends StatefulWidget {
@override
State<CustomAnimationWidget> createState() => _CustomAnimationWidgetState();
}
class _CustomAnimationWidgetState extends State<CustomAnimationWidget> {
double _rotation = 0.0;
bool _isActive = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
// ✅ 매 프레임마다 실행
if (!_isActive || !mounted) return;
setState(() {
_rotation += 0.01; // 매 프레임마다 회전
if (_rotation > 2 * 3.14159) _rotation = 0;
});
});
}
@override
void dispose() {
_isActive = false; // 플래그로 제어
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _rotation,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
);
}
}
시나리오 2: 실시간 성능 모니터링
class PerformanceMonitor {
static void init() {
int frameCount = 0;
DateTime lastSecond = DateTime.now();
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
frameCount++;
final now = DateTime.now();
if (now.difference(lastSecond).inSeconds >= 1) {
print('FPS: $frameCount'); // 초당 프레임 수 출력
frameCount = 0;
lastSecond = now;
}
});
}
}
// main.dart에서 호출
void main() {
WidgetsFlutterBinding.ensureInitialized();
PerformanceMonitor.init();
runApp(MyApp());
}
주의사항
⚠️ 제거 불가능: 한 번 등록하면 앱이 종료될 때까지 계속 실행됩니다!
// 해결책: 플래그로 제어
bool _isRunning = true;
@override
void initState() {
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
if (!_isRunning || !mounted) return; // ✅ 플래그로 중지
// ... 작업
});
}
@override
void dispose() {
_isRunning = false; // ✅ 플래그 설정
super.dispose();
}
3.3 scheduleFrameCallback()
개요
다음 프레임에서 한 번만 실행되는 콜백을 등록합니다. addPostFrameCallback과 유사하지만, 현재 프레임이 아닌 다음 프레임에서 실행됩니다.
시그니처
int scheduleFrameCallback(FrameCallback callback, {bool rescheduling = false})
사용 시나리오
void _delayedUpdate() {
// ✅ 다음 프레임에서 실행
WidgetsBinding.instance.scheduleFrameCallback((timeStamp) {
if (mounted) {
setState(() {
// 상태 업데이트
});
}
});
}
addPostFrameCallback과의 차이
// 타이밍 비교
Frame N:
build()
layout()
paint()
addPostFrameCallback() ◄── 여기 (Frame N 끝)
Frame N+1:
scheduleFrameCallback() ◄── 여기 (Frame N+1 시작)
build()
layout()
paint()
3.4 ensureInitialized()
개요
WidgetsBinding을 명시적으로 초기화합니다. runApp() 전에 Flutter 엔진 기능을 사용해야 할 때 필수입니다.
시그니처
static WidgetsBinding ensureInitialized()
사용 시나리오
시나리오 1: Firebase 초기화
void main() async {
// ✅ Flutter 엔진 초기화
WidgetsFlutterBinding.ensureInitialized();
// 이제 플랫폼 채널 사용 가능
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
시나리오 2: 화면 방향 고정
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ✅ 세로 모드만 허용
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(MyApp());
}
시나리오 3: SharedPreferences 초기화
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ✅ 앱 시작 전 설정 로드
final prefs = await SharedPreferences.getInstance();
final isDarkMode = prefs.getBool('dark_mode') ?? false;
runApp(MyApp(isDarkMode: isDarkMode));
}
시나리오 4: 네이티브 스플래시 유지
void main() {
// ✅ 바인딩 초기화 및 스플래시 유지
final binding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: binding);
runApp(MyApp());
}
왜 필요한가?
// runApp() 내부 코드 (간략화)
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized() // ◄── 자동 호출
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
runApp()이 자동으로 호출하지만, 그 이전에 플랫폼 기능을 사용하려면 명시적 호출 필요!
3.5 waitUntilFirstFrameRasterized()
개요
첫 프레임이 실제로 화면에 완전히 그려질 때까지 대기하는 Future를 반환합니다.
시그니처
Future<void> waitUntilFirstFrameRasterized()
사용 시나리오
시나리오 1: 네이티브 스플래시 제거
void main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: binding);
runApp(MyApp());
// ✅ 첫 화면이 완전히 그려진 후 스플래시 제거
await WidgetsBinding.instance.waitUntilFirstFrameRasterized();
FlutterNativeSplash.remove();
}
시나리오 2: 앱 로딩 완료 분석 이벤트
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final startTime = DateTime.now();
runApp(MyApp());
await WidgetsBinding.instance.waitUntilFirstFrameRasterized();
final loadTime = DateTime.now().difference(startTime);
// ✅ 앱 로딩 시간 분석
FirebaseAnalytics.instance.logEvent(
name: 'app_load_time',
parameters: {'duration_ms': loadTime.inMilliseconds},
);
}
프레임 렌더링 파이프라인
runApp() 호출
↓
첫 프레임 빌드 시작
↓
Layout 계산
↓
Paint (메모리에 그리기)
↓
Rasterization (GPU로 전송) ◄── waitUntilFirstFrameRasterized()가 대기하는 지점
↓
화면에 표시 완료
↓
Future 완료 ✓
3.6 addObserver() / removeObserver()
개요
앱 생명주기 이벤트를 감지하는 옵저버를 등록/제거합니다.
시그니처
void addObserver(WidgetsBindingObserver observer)
void removeObserver(WidgetsBindingObserver observer)
사용 시나리오
시나리오 1: 백그라운드 진입 시 타이머 중지
class TimerPage extends StatefulWidget {
@override
State<TimerPage> createState() => _TimerPageState();
}
class _TimerPageState extends State<TimerPage>
with WidgetsBindingObserver {
Timer? _timer;
int _counter = 0;
@override
void initState() {
super.initState();
// ✅ 옵저버 등록
WidgetsBinding.instance.addObserver(this);
_startTimer();
}
@override
void dispose() {
// ✅ 옵저버 제거 (메모리 누수 방지)
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
super.dispose();
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() => _counter++);
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
print('앱 포그라운드 복귀');
_startTimer(); // ✅ 타이머 재시작
break;
case AppLifecycleState.paused:
print('앱 백그라운드 진입');
_timer?.cancel(); // ✅ 타이머 중지 (배터리 절약)
break;
case AppLifecycleState.inactive:
print('앱 비활성 (전화 수신 등)');
break;
case AppLifecycleState.detached:
print('앱 종료 준비');
break;
case AppLifecycleState.hidden:
print('앱 숨겨짐');
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Counter: $_counter')),
);
}
}
시나리오 2: 포그라운드 복귀 시 데이터 새로고침
class DataListPage extends ConsumerStatefulWidget {
@override
ConsumerState<DataListPage> createState() => _DataListPageState();
}
class _DataListPageState extends ConsumerState<DataListPage>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// ✅ 포그라운드 복귀 시 데이터 새로고침
ref.read(dataProvider.notifier).refresh();
}
}
@override
Widget build(BuildContext context) {
final data = ref.watch(dataProvider);
return ListView(...);
}
}
시나리오 3: 앱 생명주기 전역 관리
class AppLifecycleManager extends StatefulWidget {
final Widget child;
const AppLifecycleManager({required this.child, super.key});
@override
State<AppLifecycleManager> createState() => _AppLifecycleManagerState();
}
class _AppLifecycleManagerState extends State<AppLifecycleManager>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// ✅ 전역 생명주기 이벤트 처리
switch (state) {
case AppLifecycleState.paused:
// 모든 네트워크 요청 취소
// 민감한 데이터 암호화
_handleBackground();
break;
case AppLifecycleState.resumed:
// 세션 유효성 검증
// 캐시 업데이트
_handleForeground();
break;
default:
break;
}
}
void _handleBackground() {
// 백그라운드 처리 로직
}
void _handleForeground() {
// 포그라운드 복귀 로직
}
@override
Widget build(BuildContext context) => widget.child;
}
// main.dart에서 앱 전체를 감싸기
void main() {
runApp(
AppLifecycleManager(
child: MyApp(),
),
);
}
AppLifecycleState 상세 설명
| 상태 | 의미 | 발생 시점 |
|---|---|---|
resumed |
앱이 포그라운드에서 실행 중 | 앱 시작, 백그라운드에서 복귀 |
inactive |
앱이 비활성 상태 | 전화 수신, 시스템 대화상자 표시 |
paused |
앱이 백그라운드로 이동 | 홈 버튼, 앱 전환 |
detached |
앱이 종료 준비 중 | 앱 종료 직전 |
hidden |
앱이 숨겨짐 (iOS/Android 일부) | 멀티태스킹 화면 등 |
3.7 scheduleWarmUpFrame()
개요
"워밍업 프레임"을 실행하여 첫 프레임 렌더링 성능을 최적화합니다.
시그니처
void scheduleWarmUpFrame()
사용 시나리오
void main() {
WidgetsFlutterBinding.ensureInitialized();
// ✅ 첫 프레임을 미리 준비
WidgetsBinding.instance.scheduleWarmUpFrame();
runApp(MyApp());
}
작동 원리
일반적인 앱 시작:
runApp() → 첫 프레임 빌드 → 렌더링 (약 100-200ms)
↓
사용자가 흰 화면을 잠깐 봄
scheduleWarmUpFrame() 사용:
워밍업 프레임 → runApp() → 첫 프레임 빌드 → 렌더링 (약 50-100ms)
↓
더 빠른 초기 렌더링
3.8 handleAppLifecycleStateChanged()
개요
앱 생명주기 상태를 수동으로 변경합니다. 주로 테스트 코드에서 사용됩니다.
시그니처
@protected
void handleAppLifecycleStateChanged(AppLifecycleState state)
사용 시나리오 (테스트)
testWidgets('백그라운드 진입 시 타이머 중지 테스트', (tester) async {
await tester.pumpWidget(TimerPage());
// ✅ 백그라운드 상태 시뮬레이션
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
await tester.pump();
// 타이머가 중지되었는지 검증
expect(find.text('타이머 중지됨'), findsOneWidget);
// ✅ 포그라운드 복귀 시뮬레이션
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
expect(find.text('타이머 실행 중'), findsOneWidget);
});
4. 실전 사용 패턴
4.1 Riverpod과 함께 사용하기
패턴: initState에서 안전하게 Provider 호출
class ProductListPage extends ConsumerStatefulWidget {
@override
ConsumerState<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends ConsumerState<ProductListPage> {
@override
void initState() {
super.initState();
// ✅ postFrameCallback으로 생명주기 오류 방지
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
ref.read(productProvider.notifier).loadProducts();
}
});
}
@override
Widget build(BuildContext context) {
final products = ref.watch(productProvider);
return products.when(
data: (data) => ListView.builder(...),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
}
4.2 위젯 크기 측정 및 동적 레이아웃
패턴: 자식 위젯 크기에 따라 부모 조정
class DynamicHeightContainer extends StatefulWidget {
final Widget child;
const DynamicHeightContainer({required this.child, super.key});
@override
State<DynamicHeightContainer> createState() => _DynamicHeightContainerState();
}
class _DynamicHeightContainerState extends State<DynamicHeightContainer> {
final GlobalKey _childKey = GlobalKey();
double _childHeight = 0;
@override
void initState() {
super.initState();
_measureChild();
}
void _measureChild() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final RenderBox? box = _childKey.currentContext?.findRenderObject() as RenderBox?;
if (box != null) {
setState(() {
_childHeight = box.size.height;
});
}
});
}
@override
Widget build(BuildContext context) {
return Container(
height: _childHeight > 0 ? _childHeight + 20 : null, // 패딩 추가
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: KeyedSubtree(
key: _childKey,
child: widget.child,
),
),
);
}
}
4.3 main() 함수에서 초기화
패턴: 완벽한 앱 초기화 시퀀스
void main() async {
// 1. Flutter 바인딩 초기화
final binding = WidgetsFlutterBinding.ensureInitialized();
// 2. 네이티브 스플래시 유지
FlutterNativeSplash.preserve(widgetsBinding: binding);
// 3. 화면 방향 고정
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
// 4. 상태 바 스타일 설정
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
// 5. Firebase 초기화
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 6. 로컬 저장소에서 설정 로드
final prefs = await SharedPreferences.getInstance();
final String? token = prefs.getString('auth_token');
// 7. 앱 실행
runApp(MyApp(initialToken: token));
// 8. 첫 프레임 렌더링 대기
await binding.waitUntilFirstFrameRasterized();
// 9. 스플래시 제거
FlutterNativeSplash.remove();
// 10. 앱 로딩 시간 분석
FirebaseAnalytics.instance.logEvent(name: 'app_loaded');
}
4.4 앱 생명주기 전역 관리
패턴: Provider와 함께 생명주기 관리
// 1. 생명주기 상태를 관리하는 Provider
final appLifecycleProvider = StateProvider<AppLifecycleState?>((ref) => null);
// 2. 생명주기 관리 위젯
class AppLifecycleProvider extends ConsumerStatefulWidget {
final Widget child;
const AppLifecycleProvider({required this.child, super.key});
@override
ConsumerState<AppLifecycleProvider> createState() => _AppLifecycleProviderState();
}
class _AppLifecycleProviderState extends ConsumerState<AppLifecycleProvider>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Provider 상태 업데이트
ref.read(appLifecycleProvider.notifier).state = state;
// 전역 처리
if (state == AppLifecycleState.paused) {
// 모든 타이머 중지
ref.read(timerManagerProvider.notifier).pauseAll();
// 네트워크 요청 취소
ref.read(networkManagerProvider.notifier).cancelAll();
} else if (state == AppLifecycleState.resumed) {
// 타이머 재시작
ref.read(timerManagerProvider.notifier).resumeAll();
// 데이터 새로고침
ref.read(dataProvider.notifier).refresh();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
// 3. main.dart에서 사용
void main() {
runApp(
ProviderScope(
child: AppLifecycleProvider(
child: MyApp(),
),
),
);
}
// 4. 다른 위젯에서 생명주기 상태 구독
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final lifecycleState = ref.watch(appLifecycleProvider);
return Text('앱 상태: ${lifecycleState?.toString() ?? "Unknown"}');
}
}
4.5 성능 모니터링
패턴: FPS 측정 및 로깅
class FPSMonitor {
static int _frameCount = 0;
static DateTime _lastSecond = DateTime.now();
static final List<int> _fpsHistory = [];
static void start() {
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
_frameCount++;
final now = DateTime.now();
final diff = now.difference(_lastSecond);
if (diff.inSeconds >= 1) {
final fps = _frameCount;
_fpsHistory.add(fps);
// 최근 10초 평균 FPS 계산
if (_fpsHistory.length > 10) {
_fpsHistory.removeAt(0);
}
final avgFps = _fpsHistory.reduce((a, b) => a + b) / _fpsHistory.length;
// 로깅
debugPrint('Current FPS: $fps | Avg FPS: ${avgFps.toStringAsFixed(1)}');
// 성능 저하 감지
if (avgFps < 50) {
debugPrint('⚠️ 성능 저하 감지! 평균 FPS: ${avgFps.toStringAsFixed(1)}');
}
_frameCount = 0;
_lastSecond = now;
}
});
}
static List<int> get fpsHistory => _fpsHistory;
}
// main.dart에서 활성화
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Debug 모드에서만 FPS 모니터링
if (kDebugMode) {
FPSMonitor.start();
}
runApp(MyApp());
}
5. 트러블슈팅
5.1 흔한 오류와 해결책
오류 1: "Tried to modify a provider while widget tree was building"
원인: initState()에서 직접 Provider 호출
// ❌ 잘못된 코드
@override
void initState() {
super.initState();
ref.read(myProvider.notifier).loadData(); // 오류!
}
// ✅ 올바른 코드
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
ref.read(myProvider.notifier).loadData();
}
});
}
오류 2: "setState() called after dispose()"
원인: 비동기 콜백에서 mounted 체크 누락
// ❌ 잘못된 코드
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {}); // 위젯이 이미 dispose되었을 수 있음!
});
// ✅ 올바른 코드
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
오류 3: "PlatformException: Binding not initialized"
원인: runApp() 전에 플랫폼 기능 사용
// ❌ 잘못된 코드
void main() async {
await Firebase.initializeApp(); // 오류!
runApp(MyApp());
}
// ✅ 올바른 코드
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // 먼저 초기화!
await Firebase.initializeApp();
runApp(MyApp());
}
오류 4: "Cannot add observer after dispose"
원인: dispose() 후 옵저버 제거 누락
// ❌ 잘못된 코드
class _MyState extends State<MyWidget> with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
}
// dispose에서 제거 안 함!
}
// ✅ 올바른 코드
class _MyState extends State<MyWidget> with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this); // 필수!
super.dispose();
}
}
5.2 디버깅 팁
팁 1: 프레임 콜백 로깅
void debugFrameCallbacks() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
debugPrint('[PostFrame] Callback executed at $timeStamp');
});
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
debugPrint('[PersistentFrame] Frame rendered at $timeStamp');
});
}
팁 2: 생명주기 상태 추적
class LifecycleDebugger with WidgetsBindingObserver {
void init() {
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
debugPrint('App Lifecycle Changed: $state');
debugPrint('Timestamp: ${DateTime.now()}');
debugPrint('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
}
// main.dart
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (kDebugMode) {
LifecycleDebugger().init();
}
runApp(MyApp());
}
6. 성능 최적화
6.1 불필요한 콜백 제거
// ❌ 성능 저하: 매 프레임마다 복잡한 계산
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
// 매우 무거운 작업
_expensiveComputation();
});
// ✅ 최적화: 필요할 때만 실행
bool _isComputationNeeded = true;
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
if (!_isComputationNeeded) return; // 조기 종료
_expensiveComputation();
_isComputationNeeded = false; // 플래그 설정
});
6.2 프레임 드롭 감지
class FrameDropDetector {
static Duration? _lastFrameTime;
static const Duration _targetFrameTime = Duration(milliseconds: 16); // 60fps
static void start() {
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
if (_lastFrameTime != null) {
final frameDuration = timeStamp - _lastFrameTime!;
if (frameDuration > _targetFrameTime * 2) {
debugPrint('⚠️ Frame drop detected: ${frameDuration.inMilliseconds}ms');
}
}
_lastFrameTime = timeStamp;
});
}
}
6.3 메모리 누수 방지 체크리스트
-
addObserver()를 사용했다면removeObserver()호출했는가? -
addPersistentFrameCallback()사용 시 플래그로 제어하는가? - 모든 비동기 콜백에
mounted체크를 했는가? -
dispose()에서 모든 리소스를 정리했는가?
7. 참고 자료
7.1 공식 문서
- WidgetsBinding API 문서
- https://api.flutter.dev/flutter/widgets/WidgetsBinding-class.html
- 모든 메서드와 속성 상세 설명
- WidgetsBindingObserver API 문서
- SchedulerBinding API 문서
- https://api.flutter.dev/flutter/scheduler/SchedulerBinding-class.html
- 프레임 스케줄링 관련 메서드 (WidgetsBinding의 부모)
- Flutter 아키텍처 개요
- Flutter 성능 최적화 가이드
- https://docs.flutter.dev/perf/best-practices
- 프레임 성능 개선 방법
7.2 심화 학습
- Flutter YouTube - The Boring Show
- https://www.youtube.com/playlist?list=PLjxrf2q8roU3ahJVrSgAnPjzkpGmL9Czl
- Flutter 팀의 실전 개발 팁
- Flutter Cookbook
- https://docs.flutter.dev/cookbook
- 실전 예제 모음
- Flutter Performance Profiling
- https://docs.flutter.dev/perf/ui-performance
- 성능 프로파일링 도구 사용법
7.3 커뮤니티
- Stack Overflow - Flutter WidgetsBinding
- https://stackoverflow.com/questions/tagged/flutter+widgetsbinding
- 실제 사용 시 발생하는 질문들
- Flutter Dev Community
- https://dev.to/t/flutter
- 개발자들의 경험 공유
- Flutter Community Medium
- https://medium.com/flutter-community
- "Understanding WidgetsBinding" 검색
7.4 프로젝트 내 참고 파일
.github/troubleshooting/riverpod_lifecycle_error_fix.md- Riverpod 생명주기 오류 해결.github/data_fetching_strategy.md- Provider 사용 전략.github/copilot-instructions.md- 프로젝트 컨벤션
부록
A. 전체 메서드 요약표
| 메서드 | 실행 시점 | 실행 횟수 | 주요 용도 | 제거 가능 |
|---|---|---|---|---|
addPostFrameCallback |
현재 프레임 끝 | 1회 | initState에서 context 접근 | 자동 |
scheduleFrameCallback |
다음 프레임 | 1회 | 프레임 지연 실행 | 자동 |
addPersistentFrameCallback |
매 프레임 | 영구적 | 애니메이션, 게임 루프 | 불가 |
ensureInitialized |
main() 시작 시 | 1회 | 플랫폼 API 초기화 | - |
waitUntilFirstFrameRasterized |
첫 프레임 완료 후 | 1회 | 스플래시 제거 | - |
addObserver |
생명주기 변화 시 | 이벤트마다 | 백그라운드/포그라운드 감지 | 필수 |
scheduleWarmUpFrame |
앱 시작 시 | 1회 | 초기 렌더링 최적화 | - |
B. 빠른 참조 코드
B.1 기본 템플릿
import 'package:flutter/material.dart';
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
// 생명주기 옵저버 등록
WidgetsBinding.instance.addObserver(this);
// 프레임 완료 후 초기화
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_initialize();
}
});
}
@override
void dispose() {
// 옵저버 제거 (필수!)
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// 생명주기 이벤트 처리
}
void _initialize() {
// 초기화 로직
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Page')),
body: Container(),
);
}
}
B.2 Riverpod 템플릿
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyPage extends ConsumerStatefulWidget {
@override
ConsumerState<MyPage> createState() => _MyPageState();
}
class _MyPageState extends ConsumerState<MyPage>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// ✅ 안전한 Provider 호출
ref.read(myProvider.notifier).loadData();
}
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.read(myProvider.notifier).refresh();
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(myProvider);
return Scaffold(
body: state.when(
data: (data) => ListView(...),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
),
);
}
}'새로 알게된 지식들' 카테고리의 다른 글
| 언젠가 하겠지? — 그 “언젠가”는 절대 오지 않는다. (4) | 2025.11.08 |
|---|---|
| [Flutter] Google Maps Cloud 스타일 설정 가이드 (0) | 2025.11.06 |
| [flutter] google_maps_flutter 관련 정리 (0) | 2025.09.22 |
| VS Code Copilot Agent와 Figma MCP 연결 가이드 (4) | 2025.07.29 |
| certbot 인증서 발급 방법 (1) | 2025.07.24 |