
앱 켜자마자 데이터가 필요할 때 (비동기 초기화 패턴)
로그인 정보나 설정을 불러오기 전에 메인 화면이 먼저 떠버립니다. main() 함수에서 기다려야 할까요, 아니면 스플래시 화면을 만들어야 할까요? 3가지 초기화 전략을 비교합니다.

로그인 정보나 설정을 불러오기 전에 메인 화면이 먼저 떠버립니다. main() 함수에서 기다려야 할까요, 아니면 스플래시 화면을 만들어야 할까요? 3가지 초기화 전략을 비교합니다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

왜 CPU는 빠른데 컴퓨터는 느릴까? 80년 전 고안된 폰 노이만 구조의 혁명적인 아이디어와, 그것이 남긴 치명적인 병목현상에 대해 정리했습니다.

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

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

앱을 켜면 저장된 토큰을 확인해서 '로그인 화면'이나 '홈 화면' 중 하나로 보내야 합니다. 그런데 아주 짧은 순간(0.1초), 홈 화면의 껍데기가 보였다가 로그인 화면으로 튕기는 "Flash of Unstyled Content (FOUC)" 현상이 발생합니다.
"데이터를 다 불러오고 나서 화면을 띄울 순 없나요?"
비동기(Async) 세계인 Flutter에서 "준비될 때까지 기다려(Blocking)"는 가장 까다로운 주제, 그리고 가장 중요한 첫인상(First Impression) 문제입니다.
main()에서 기다리기 (가장 단순함)가장 쉬운 방법은 앱이 실행(runApp)되기 전에 모든 것을 끝내는 것입니다.
void main() async {
// 1. Flutter 엔진 초기화 필수
WidgetsFlutterBinding.ensureInitialized();
// 2. 비동기 작업 대기 (예: SharedPreferences 로드)
await UserPreferences.init();
final isLoggedIn = await AuthService.checkLogin();
// 3. 준비 끝난 후 앱 실행
runApp(MyApp(startPage: isLoggedIn ? Home() : Login()));
}
장점:
단점:
네이티브 로딩 화면은 빨리 넘기고, Flutter가 그린 로딩 화면을 보여주는 방식입니다. 사용자에게 "앱이 켜졌고, 데이터를 불러오는 중이다"라는 피드백을 줄 수 있습니다.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: InitService.initialize(), // 여기서 병렬 초기화 추천
builder: (context, snapshot) {
// 1. 로딩 중이면 스플래시 화면 표시
if (snapshot.connectionState == ConnectionState.waiting) {
return SplashScreen();
}
// 2. 에러 처리 (인터넷 연결 끊김 등)
if (snapshot.hasError) {
return ErrorScreen(onRetry: () {
// setState로 Future 다시 트리거
});
}
// 3. 완료되면 메인 앱 시작
return MaterialApp(home: Home());
},
);
}
}
장점:
main()에서 실패하면 앱이 크래시(Crash) 납니다.AppStartupWidget (고급 패턴)상태 관리 라이브러리를 쓴다면, 초기화 로직도 상태로 관리하는 게 좋습니다. Riverpod 창시자 Remi Rousselet가 추천하는 Eager Initialization 패턴입니다.
// 1. 초기화 로직을 담은 Provider
// FutureProvider로 만들면 캐싱과 에러 핸들링이 공짜입니다.
final appStartupProvider = FutureProvider<void>((ref) async {
// 병렬 실행으로 속도 최적화
await Future.wait([
ref.watch(sharedPrefsProvider.future),
ref.watch(authProvider.future),
ref.watch(remoteConfigProvider.future),
]);
});
// 2. 초기화를 감시하는 위젯
class AppStartupWidget extends ConsumerWidget {
final WidgetBuilder onLoaded;
const AppStartupWidget({required this.onLoaded});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 3. 초기화 상태 구독
final startupStatus = ref.watch(appStartupProvider);
return startupStatus.when(
data: (_) => onLoaded(context), // 성공 시 자식 위젯 렌더링
loading: () => const SplashScreen(),
error: (e, st) => ErrorScreen(e, onRetry: () => ref.invalidate(appStartupProvider)),
);
}
}
이 패턴의 강력한 점은 의존성 주입(DI)이 완벽하게 해결된다는 점입니다.
onLoaded가 호출된 시점에는 sharedPrefsProvider가 이미 준비된 상태임이 보장됩니다.
초기화할 게 많을 때 순차적으로 (await A; await B; await C;) 하면 시간이 줄줄 샙니다.
독립적인 작업이라면 반드시 병렬로 처리하세요.
// ❌ 1초 + 1초 + 1초 = 3초
await initAds();
await initAnalytics();
await initUser();
// ✅ 동시에 시작 = 1초 (가장 느린 작업 기준)
await Future.wait([
initAds(),
initAnalytics(),
initUser(),
]);
단, 순서가 중요한 작업(예: Firebase 초기화 -> Analytics 초기화)은 순차적으로 해야 합니다.
전략 2, 3을 쓰더라도, Flutter 엔진이 예열되는 아주 짧은 시간(약 0.5초) 동안은 여전히 흰색이나 검은색 화면이 나올 수 있습니다.
이것까지 완벽하게 잡으려면 flutter_native_splash 패키지를 써야 합니다.
pubspec.yaml에 설정을 적어두면, 네이티브(Android/iOS) 프로젝트의 Launch Screen을 자동으로 만들어줍니다.
Flutter가 첫 프레임(First Frame)을 그릴 때까지 네이티브 이미지가 버텨주므로, 사용자는 앱이 "켜지자마자 로딩 화면이 떴다"고 느낍니다.
가끔 위젯이 아니라, 일반 클래스나 함수에서 초기화를 기다려야 할 때가 있습니다.
"API 클라이언트(RestClient)가 토큰이 준비될 때까지 요청을 보류하고 싶다" 같은 경우죠.
이때 Completer를 씁니다.
class TokenService {
final Completer<String> _completer = Completer();
// 초기화 함수 (어딘가에서 호출됨)
void setToken(String token) {
if (!_completer.isCompleted) _completer.complete(token);
}
// 토큰이 필요하면 이걸 호출
Future<String> getToken() {
return _completer.future; // 토큰 들어올 때까지 무한 대기
}
}
이렇게 하면 await getToken() 하는 녀석들은 setToken이 불릴 때까지 얌전히 줄 서서 기다립니다. 동기화(Synchronization)의 기본기입니다.
초기화 로직에 Future.wait을 쓸 때 가장 무서운 건 "하나라도 영원히 안 끝나면 앱이 영원히 안 켜진다"는 겁니다.
특히 서드파티 SDK(광고, 분석 툴)가 네트워크 문제로 응답이 없으면 대재앙이 일어납니다.
반드시 타임아웃을 거세요.
try {
await Future.wait([
initCritical(),
initOptional().timeout(Duration(seconds: 2)), // 👈 2초 지나면 버림
]);
} catch (e) {
// 타임아웃 나도 앱은 켜야 함
print("일부 초기화 실패: $e");
}
runApp(MyApp());
"광고 초기화 실패했으니 쇼핑몰 앱을 안 켜주겠다"는 건 주객전도입니다. 핵심 코어와 부가 기능을 분리하세요.
제가 만든 첫 앱은 main() 함수에서 무거운 API를 호출했습니다.
와이파이가 느린 환경에서 테스트해보니, 앱 아이콘을 누르고 5초 동안 아무 반응이 없었습니다.
QA 팀에서는 "앱이 터치 인식을 안 해요"라고 리포트했습니다.
사실은 await apiCall()에 갇혀서 runApp()까지 도달을 못한 것이었죠.
해결:
초기화 로직을 FutureBuilder + SplashScreen으로 옮겼습니다.
앱은 0.1초 만에 켜졌고, "데이터 로딩 중..."이라는 스피너가 돌았습니다.
기능은 똑같지만, 사용자는 "앱이 빠릿하다"라고 느꼈습니다.
UX의 핵심은 '기다리지 않게 하는 것'이 아니라 '기다리고 있다는 걸 알려주는 것'입니다.
Q: 초기화 실패하면 앱 꺼야 하나요?
A: 절대 안 됩니다. "네트워크 오류가 발생했습니다. 재시도하시겠습니까?" 버튼을 보여줘야 합니다. main()에서 에러가 나면 복구할 방법이 없지만, 위젯 레벨에서는 setState나 ref.refresh로 재시도가 가능합니다.
Q: 권한 요청(위치, 알림)은 언제 해요? A: 초기화 때 하지 마세요. 앱 켜자마자 "알림 허용하시겠습니까?" 물어보면 99% 거절합니다. 해당 기능이 필요할 때(예: 지도 탭 누를 때) 요청하세요.
main()에서 await 해도 됨. (SharedPrefs 정도)