Flutter에서 비동기 작업(API 호출, Future, async/await, Stream)을 다룰 때 자주 발생하는 오류들을 정리했습니다. 각 오류의 원인, 오류 메시지, 해결방법을 구체적인 코드 예제와 함께 설명합니다.
📋 목차
1. ⚠️ dispose() 후 setState() 호출 오류
❌ 오류 메시지
setState() called after dispose(): _UserProfileState#12345
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree.
🔍 원인 설명
위젯이 dispose()된 후에도 비동기 작업(API 호출 등)이 완료되어 setState()를 호출할 때 발생합니다. 이는 메모리 누수와 앱 크래시로 이어질 수 있습니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatefulWidget {
final String userId;
const UserProfile({Key? key, required this.userId}) : super(key: key);
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State {
Map<String, dynamic>? user;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/${widget.userId}')
);
if (response.statusCode == 200) {
// 위젯이 dispose된 후에도 실행될 수 있음
setState(() { // ⚠️ 오류 발생 가능
user = json.decode(response.body);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: user == null
? CircularProgressIndicator()
: Text(user!['name']),
);
}
}
✅ 해결방법 1: mounted 체크 사용
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class _UserProfileState extends State {
Map<String, dynamic>? user;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/${widget.userId}')
);
if (response.statusCode == 200) {
// mounted 체크로 위젯이 여전히 트리에 있는지 확인
if (mounted) { // ✅ mounted 체크
setState(() {
user = json.decode(response.body);
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: user == null
? CircularProgressIndicator()
: Text(user!['name']),
);
}
}
✅ 해결방법 2: dispose()에서 취소
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class _UserProfileState extends State {
Map<String, dynamic>? user;
Timer? _timer;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/${widget.userId}')
);
if (response.statusCode == 200 && mounted) {
setState(() {
user = json.decode(response.body);
});
}
}
@override
void dispose() {
_timer?.cancel(); // ✅ 타이머 취소
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: user == null
? CircularProgressIndicator()
: Text(user!['name']),
);
}
}
2. ⚠️ Future를 직접 위젯에 렌더링하는 오류
❌ 오류 메시지
A build function returned null.
The widget returned by the build function must not return null.
🔍 원인 설명
Future 객체를 직접 위젯에 렌더링하려고 할 때 발생합니다. Flutter는 Future 객체를 위젯으로 렌더링할 수 없습니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatelessWidget {
final String userId;
const UserProfile({Key? key, required this.userId}) : super(key: key);
Future<Map<String, dynamic>> _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$userId')
);
return json.decode(response.body);
}
@override
Widget build(BuildContext context) {
final userFuture = _fetchUser(); // ⚠️ Future 객체
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: Text(userFuture.toString()), // ⚠️ Future를 직접 렌더링
);
}
}
✅ 해결방법 1: FutureBuilder 사용
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatelessWidget {
final String userId;
const UserProfile({Key? key, required this.userId}) : super(key: key);
Future<Map<String, dynamic>> _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$userId')
);
return json.decode(response.body);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: FutureBuilder<Map<String, dynamic>>(
future: _fetchUser(), // ✅ FutureBuilder 사용
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Text('No data');
}
return Text(snapshot.data!['name']);
},
),
);
}
}
✅ 해결방법 2: StatefulWidget에서 상태 관리
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatefulWidget {
final String userId;
const UserProfile({Key? key, required this.userId}) : super(key: key);
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State {
Map<String, dynamic>? user;
bool isLoading = true;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/${widget.userId}')
);
if (mounted) {
setState(() {
user = json.decode(response.body);
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: isLoading
? CircularProgressIndicator()
: user == null
? Text('No data')
: Text(user!['name']),
);
}
}
3. ⚠️ BuildContext 사용 오류 (비동기 후)
❌ 오류 메시지
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
🔍 원인 설명
비동기 작업 후 이미 dispose된 위젯의 BuildContext를 사용할 때 발생합니다. Navigator.push()나 Scaffold.of() 등을 비동기 후에 호출하면 오류가 발생합니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State {
Future _login() async {
// 비동기 작업
await Future.delayed(Duration(seconds: 2));
// 위젯이 dispose된 후에도 실행될 수 있음
Navigator.pushReplacement( // ⚠️ 오류 발생 가능
context, // ⚠️ 이미 dispose된 context 사용
MaterialPageRoute(builder: (context) => HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: _login,
child: Text('Login'),
),
);
}
}
✅ 해결방법 1: mounted 체크
import 'package:flutter/material.dart';
class _LoginPageState extends State {
Future _login() async {
await Future.delayed(Duration(seconds: 2));
// mounted 체크로 위젯이 여전히 트리에 있는지 확인
if (!mounted) return; // ✅ mounted 체크
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: _login,
child: Text('Login'),
),
);
}
}
✅ 해결방법 2: context 저장
import 'package:flutter/material.dart';
class _LoginPageState extends State {
Future _login() async {
final navigator = Navigator.of(context); // ✅ context를 미리 저장
await Future.delayed(Duration(seconds: 2));
if (!mounted) return;
navigator.pushReplacement(
MaterialPageRoute(builder: (context) => HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: _login,
child: Text('Login'),
),
);
}
}
4. ⚠️ FutureBuilder 사용 오류
❌ 오류 메시지
A build method returned null.
The widget returned by the build function must not return null.
🔍 원인 설명
FutureBuilder의 builder가 null을 반환하거나, 모든 상태를 처리하지 않아 발생합니다. FutureBuilder는 모든 ConnectionState를 처리해야 합니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
class UserList extends StatelessWidget {
Future<List> _fetchUsers() async {
await Future.delayed(Duration(seconds: 2));
return ['User1', 'User2', 'User3'];
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List>(
future: _fetchUsers(),
builder: (context, snapshot) {
// ⚠️ waiting 상태를 처리하지 않음
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return ListTile(title: Text(snapshot.data![index]));
},
);
}
// ⚠️ 다른 상태에서 null 반환
},
);
}
}
✅ 해결방법: 모든 상태 처리
import 'package:flutter/material.dart';
class UserList extends StatelessWidget {
Future<List> _fetchUsers() async {
await Future.delayed(Duration(seconds: 2));
return ['User1', 'User2', 'User3'];
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List>(
future: _fetchUsers(),
builder: (context, snapshot) {
// ✅ 모든 상태 처리
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Text('No users found');
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return ListTile(title: Text(snapshot.data![index]));
},
);
},
);
}
}
5. ⚠️ Stream 구독 해제 누락 오류
❌ 오류 메시지
Bad state: Stream has already been listened to.
Memory leak: Stream subscription not cancelled.
🔍 원인 설명
Stream 구독을 해제하지 않아 메모리 누수가 발생합니다. 위젯이 dispose된 후에도 Stream이 계속 이벤트를 발생시키면 setState() 오류가 발생할 수 있습니다.
❌ 문제가 있는 코드
import 'dart:async';
import 'package:flutter/material.dart';
class TimerWidget extends StatefulWidget {
@override
_TimerWidgetState createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State {
int _counter = 0;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
// Stream 구독
final stream = Stream.periodic(Duration(seconds: 1), (i) => i);
_subscription = stream.listen((value) {
setState(() {
_counter = value;
});
});
// ⚠️ dispose에서 구독 해제하지 않음
}
@override
Widget build(BuildContext context) {
return Text('Counter: $_counter');
}
}
✅ 해결방법 1: dispose()에서 구독 해제
import 'dart:async';
import 'package:flutter/material.dart';
class _TimerWidgetState extends State {
int _counter = 0;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
final stream = Stream.periodic(Duration(seconds: 1), (i) => i);
_subscription = stream.listen((value) {
if (mounted) {
setState(() {
_counter = value;
});
}
});
}
@override
void dispose() {
_subscription?.cancel(); // ✅ 구독 해제
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Counter: $_counter');
}
}
✅ 해결방법 2: StreamBuilder 사용
import 'package:flutter/material.dart';
class TimerWidget extends StatelessWidget {
final Stream _stream = Stream.periodic(
Duration(seconds: 1),
(i) => i
);
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _stream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return Text('Counter: ${snapshot.data}');
},
);
}
}
6. ⚠️ async/await 누락 오류
❌ 오류 메시지
The return type 'Future' isn't a 'Widget', as required by the method 'build'.
The body might complete normally, causing 'null' to be returned.
🔍 원인 설명
비동기 함수를 호출할 때 await를 사용하지 않아 Future 객체가 반환되거나, build 메서드가 async로 선언되어 발생합니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
class UserProfile extends StatelessWidget {
Future _fetchUserName() async {
await Future.delayed(Duration(seconds: 1));
return 'John Doe';
}
@override
Widget build(BuildContext context) {
// ⚠️ await 없이 Future 반환
final name = _fetchUserName(); // Future 반환
return Text(name); // ⚠️ Future를 직접 사용
}
}
✅ 해결방법 1: FutureBuilder 사용
import 'package:flutter/material.dart';
class UserProfile extends StatelessWidget {
Future _fetchUserName() async {
await Future.delayed(Duration(seconds: 1));
return 'John Doe';
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _fetchUserName(), // ✅ FutureBuilder 사용
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text(snapshot.data ?? 'No name');
},
);
}
}
✅ 해결방법 2: StatefulWidget에서 상태 관리
import 'package:flutter/material.dart';
class UserProfile extends StatefulWidget {
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State {
String? _name;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadName();
}
Future _loadName() async {
await Future.delayed(Duration(seconds: 1));
final name = 'John Doe';
if (mounted) {
setState(() {
_name = name;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return CircularProgressIndicator();
}
return Text(_name ?? 'No name');
}
}
7. ⚠️ Future 에러 처리 누락
❌ 오류 메시지
Uncaught exception: Exception: Failed to load data
Unhandled exception: SocketException: Failed host lookup
🔍 원인 설명
Future의 에러를 처리하지 않아 발생합니다. 네트워크 오류나 서버 오류 시 사용자에게 적절한 피드백을 제공하지 못하고 앱이 크래시될 수 있습니다.
❌ 문제가 있는 코드
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatefulWidget {
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State {
Map<String, dynamic>? user;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/1')
);
// ⚠️ 에러 처리 없음
final data = json.decode(response.body);
setState(() {
user = data;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: user == null
? CircularProgressIndicator()
: Text(user!['name']),
);
}
}
✅ 해결방법 1: try-catch 사용
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class _UserProfileState extends State {
Map<String, dynamic>? user;
String? error;
bool isLoading = true;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/1')
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (mounted) {
setState(() {
user = data;
error = null;
isLoading = false;
});
}
} else {
throw Exception('Failed to load user: ${response.statusCode}');
}
} catch (e) {
if (mounted) {
setState(() {
error = e.toString();
isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return CircularProgressIndicator();
}
if (error != null) {
return Text('Error: $error');
}
return Text(user!['name']);
}
}
✅ 해결방법 2: FutureBuilder에서 에러 처리
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserProfile extends StatelessWidget {
Future<Map<String, dynamic>> _fetchUser() async {
final response = await http.get(
Uri.parse('https://api.example.com/users/1')
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load user');
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: _fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) { // ✅ 에러 처리
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Text('No data');
}
return Text(snapshot.data!['name']);
},
);
}
}
✅ 마무리
Flutter에서 비동기 작업을 다룰 때 발생하는 주요 오류 7가지를 정리했습니다. 각 오류의 원인과 해결방법을 이해하면 더 안정적인 Flutter 애플리케이션을 개발할 수 있습니다.
주요 포인트:
- mounted 체크: dispose된 위젯에서 setState() 호출 방지
- FutureBuilder/StreamBuilder: 비동기 데이터를 안전하게 렌더링
- 구독 해제: Stream 구독을 dispose()에서 반드시 해제
- 에러 처리: 모든 Future에 try-catch 또는 .catchError() 추가
- BuildContext 관리: 비동기 후 context 사용 시 mounted 체크 필수
💡 참고: Flutter 3.0부터는 더 나은 에러 핸들링과 디버깅 도구가 제공됩니다. 또한 Provider나 Riverpod 같은 상태 관리 라이브러리를 사용하면 비동기 상태를 더 효율적으로 관리할 수 있습니다.

카카오톡 오픈채팅 링크
https://open.kakao.com/o/seCteX7h
'개발 > FRONT' 카테고리의 다른 글
| ⚠️ Can't perform a React state update on an unmounted component / Cannot read property 'name' of undefined React 오류 정리 (0) | 2025.12.27 |
|---|---|
| Info.plist 개인정보 사용 목적 누락(ITMS-90683) 오류 해결방법 (1) | 2025.12.23 |
| 웹/하이브리드 앱에서 PDF 생성 후 다운로드하기 (0) | 2025.12.23 |
| Capacitor 환경에서 Google 광고 AdMob 붙이기 (0) | 2025.12.23 |
| npm cache 때문에 수정이 적용되지 않을 때 해결방법 npmcache 삭제 (0) | 2025.12.23 |