
ProviderNotFoundException 해결법 (Context의 비밀)
분명히 Provider로 감쌌는데 찾을 수 없다고 에러가 뜹니다. BuildContext와 위젯 트리의 '족보' 관계를 이해하면, 이 에러는 다시는 당신을 괴롭히지 못합니다.

분명히 Provider로 감쌌는데 찾을 수 없다고 에러가 뜹니다. BuildContext와 위젯 트리의 '족보' 관계를 이해하면, 이 에러는 다시는 당신을 괴롭히지 못합니다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

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

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

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

Flutter 입문자가 Provider를 쓸 때 100% 마주치는 에러가 있습니다.
Error: Could not find the correct Provider<User> above this MyWidget Widget
코드를 다시 봅니다. 분명히 ChangeNotifierProvider로 MyWidget을 감쌌습니다.
// ❌ 입문자의 흔한 실수
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserProvider(),
child: MyWidget(), // 👈 여기서 UserProvider를 찾으려고 함
);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 💥 펑! 여기서 에러 발생
final user = Provider.of<UserProvider>(context);
return Text(user.name);
}
}
"아니, 바로 위에 감싸져 있잖아! 왜 못 찾아?" 이것은 여러분이 BuildContext와 위젯 트리 탐색 원리를 오해했기 때문입니다.
Provider.of(context) (또는 context.read/watch)는 이렇게 작동합니다.
"지금 주어진 context(현재 위치)에서 시작해서, 위쪽(부모) 방향으로 트리를 타고 올라가며 가장 먼저 만나는 Provider를 찾아라."
그런데 위의 예제 코드를 다시 볼까요?
실제로 MyWidget 안에서 호출된 Provider.of(context)는 문제가 없습니다. MyWidget의 부모가 ChangeNotifierProvider니까요.
하지만! 만약 여러분이 실수로 이런 구조를 만들었다면 어떨까요?
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserProvider(),
child: Scaffold(
body: Builder( // 👈 Builder가 없었다면?
builder: (context) {
// ✅ 여기서는 찾을 수 있음 (Scaffold -> Provider)
return Text(Provider.of<UserProvider>(context).name);
}
),
),
);
}
}
가장 흔한 실수는 같은 build 메서드 안에서 Provider를 만들고, 바로 그 아래에서 소비(Consume)하려고 할 때 발생합니다.
// ❌ 진짜 문제의 코드
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserProvider(),
child: Text(
// 💥 여기서 context는 'ChangeNotifierProvider'의 부모 context임!
Provider.of<UserProvider>(context).name
),
);
}
여기서 사용된 context는 ChangeNotifierProvider를 만들고 있는 부모의 context입니다.
즉, Provider보다 더 위에 있는 위치입니다.
자기 자신보다 위에 있는 Provider를 찾으라고 하니, 당연히 없다고 에러가 나는 겁니다. (자식은 아직 태어나지도 않았으니까요!)
가장 깔끔한 해결책은 Provider를 제공하는 곳과 사용하는 곳을 분리하는 것입니다.
class ParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider( // 1. 여기서 제공하고
create: (_) => UserProvider(),
child: ChildWidget(), // 2. 자식 위젯을 따로 만듦
);
}
}
class ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 3. 자식의 context는 Provider 아래에 있음 -> 성공!
final user = Provider.of<UserProvider>(context);
return Text(user.name);
}
}
이제 ChildWidget의 build(context)에 들어오는 context는 Provider의 자식 위치이므로, 위를 쳐다보면 Provider가 보입니다.
새로운 위젯 클래스를 만들기 귀찮다면, Builder 위젯을 써서 새로운 context를 중간에 만들어주면 됩니다.
ChangeNotifierProvider(
create: (_) => UserProvider(),
child: Builder( // 👈 새로운 context 생성기
builder: (newContext) {
// 이 newContext는 Provider 아래에 있음!
return Text(Provider.of<UserProvider>(newContext).name);
},
),
)
Builder는 단순히 "새로운 build 메서드(Scope)"를 열어주는 역할을 합니다.
Provider 패키지가 제공하는 Consumer 위젯을 쓰면 더 좋습니다. 이건 내부적으로 Builder와 똑같은 원리지만, 문법이 더 명시적입니다.
ChangeNotifierProvider(
create: (_) => UserProvider(),
child: Consumer<UserProvider>( // 👈 Provider를 찾아서 builder에 넘겨줌
builder: (context, userProvider, child) {
return Text(userProvider.name);
},
),
)
Consumer는 Provider를 찾지 못하면 에러를 뱉는 대신, 명확하게 어디서부터 찾는지 알 수 있게 해줍니다.
또 다른 흔한 실수는 Dialog나 Navigator.push를 할 때 발생합니다.
다이얼로그나 새로운 페이지는 현재 위젯 트리와는 별개의 트리(Overlay)에 붙는 경우가 많습니다.
그래서 App 최상단(MaterialApp 위)에 Provider를 두지 않고, 특정 페이지 안에 둔 Provider는 다이얼로그에서 접근할 수 없는 경우가 많습니다.
해결책:
전역적으로 쓰이는 Provider(User, Auth, Theme 등)는 반드시 MaterialApp보다 상위(최상단)에 두세요.
그래야 어떤 팝업, 다이얼로그, 페이지 이동이 일어나도 context.read로 접근할 수 있습니다.
Navigator.push로 새 페이지를 띄우거나, showDialog, showModalBottomSheet를 띄울 때 왜 에러가 날까요?
이 녀석들은 기술적으로 현재 위젯 트리(Local Tree)의 자식이 아닙니다.
MaterialApp이 관리하는 별도의 Overlay 트리(Stack 구조) 위에 둥둥 떠 있는 존재들입니다.
MaterialApp
├─ Provider (GlobalProvider)
├─ HomePage (LocalProvider)
│ └─ Button
└─ Overlay (여기에 Dialog가 붙음!)
└─ DialogWidget
만약 LocalProvider가 HomePage 안에 있다면, Overlay에 붙은 DialogWidget은 LocalProvider를 볼 수 없습니다. 부모가 아니니까요!
하지만 GlobalProvider는 MaterialApp보다 위에 있으니, Overlay에서도 보입니다.
실제 겪은 사례입니다.
로그인 버튼을 누르면 바텀 시트(showModalBottomSheet)가 뜨고, 거기서 이메일을 입력받습니다.
이때 AuthProvider를 통해 로그인을 시도하는데, ProviderNotFoundException이 터집니다.
// HomePage.dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthProvider(), // ❌ 여기서 만들고
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => LoginSheet(), // ❌ 여기서 씀
);
},
),
),
);
}
}
LoginSheet는 HomePage의 자식이 아니라 Overlay의 자식입니다.
그래서 HomePage 내부의 AuthProvider에 접근할 수 없습니다.
AuthProvider를 main.dart의 MaterialApp 위로 옮겼습니다.
// main.dart
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
],
child: MaterialApp(...),
),
);
}
이제 어디서든(페이지, 다이얼로그, 바텀 시트) AuthProvider에 접근할 수 있게 되었습니다.
상태 관리는 "최소한의 스코프"가 좋지만, "내비게이션이 개입하는 데이터"라면 과감하게 전역으로 빼는 게 정신 건강에 좋습니다.
initState의 함정문제:
initState에서 Provider의 데이터를 초기화하거나 API를 호출하고 싶습니다.
@override
void initState() {
super.initState();
// 💥 에러 발생: dependOnInheritedWidgetOfExactType() ...
final user = Provider.of<UserProvider>(context, listen: false);
user.fetchProfile();
}
initState 시점에는 아직 context가 위젯 트리에 완전히 연결되지 않아서 찾을 수 없거나, 불안정할 수 있습니다.
도전: 에러 없이 Provider를 호출하는 3가지 방법을 찾아보세요.
정답:Future.microtask(() => context.read<P>().fet...): 현재 프레임 끝난 직후 실행.WidgetsBinding.instance.addPostFrameCallback((_) => ...): 화면 다 그려진 후 실행.Provider의 제작자는 이 ProviderNotFoundException과 Context 지옥에 지쳐서 Riverpod을 다시 만들었습니다.
Riverpod은 Context 대신 전역적인 ProviderContainer (Scope)에서 Ref라는 객체를 씁니다.
// Provider: BuildContext 필수 (트리 위계 중요)
Provider.of<User>(context);
// Riverpod: context 필요 없음 (어디서든 호출 가능)
ref.read(userProvider);
여러분이 만약 아키텍처를 새로 짠다면, "트리 구조에 의존적인 데이터(테마, 다국어)"는 Provider를 쓰고, "비즈니스 로직(Auth, 유저 정보, 장바구니)"는 Riverpod을 쓰는 하이브리드 전략을 추천합니다.
Q: GetIt을 쓰면 안 되나요?
A: 됩니다. GetIt은 Context에 의존하지 않고 싱글톤을 전역에서 가져오는 서비스 로케이터 패턴입니다. Provider의 Context 종속성이 싫다면 좋은 대안입니다. 단, 위젯 리빌드(Rebuild)를 자동으로 안 해주니, 데이터 변경 시 ValueListenable 등을 섞어 써야 합니다.
Q: context.read vs context.watch?
A:
watch: 데이터 바뀌면 화면 다시 그림. (build() 안에서 사용)read: 데이터만 가져오고 다시 안 그림. (onPressed 같은 이벤트 핸들러 안에서 사용)onPressed 안에서 watch 쓰면 에러 납니다.Q: Provider.of(context, listen: false)가 read인가요?
A: 네, 맞습니다. 짧게 쓰려고 read라는 확장 메서드(Extension)가 생긴 겁니다.
ProviderNotFoundException은 "족보" 문제입니다.