본문 바로가기

개발/FRONT

⚠️ Flutter 비동기 오류 setState() called after dispose(),A build function returned null,Looking up a deactivated widget's ancestor is unsafe. 해결방법

728x90

 

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

 


 

728x90