Flutter에서 인터넷 연결 상태를 어떻게 확인하나요?

질문

Flutter 앱에서 인터넷 연결 상태를 확인하고 관리하는 방법을 설명해주세요.

답변

Flutter 앱에서 인터넷 연결 상태를 확인하고 모니터링하는 것은 오프라인 기능 구현, 네트워크 오류 처리, 사용자 경험 개선 등에 중요합니다. Flutter에서는 여러 방법으로 네트워크 연결 상태를 확인할 수 있습니다.

1. connectivity_plus 패키지 사용하기

가장 널리 사용되는 방법은 connectivity_plus 패키지를 사용하는 것입니다:

// pubspec.yaml
dependencies:
  connectivity_plus: ^4.0.2

1.1 현재 연결 상태 확인하기

import 'package:connectivity_plus/connectivity_plus.dart';

Future<void> checkConnectivity() async {
  var connectivityResult = await Connectivity().checkConnectivity();

  if (connectivityResult == ConnectivityResult.mobile) {
    print('모바일 데이터 네트워크에 연결되어 있습니다.');
  } else if (connectivityResult == ConnectivityResult.wifi) {
    print('WiFi 네트워크에 연결되어 있습니다.');
  } else if (connectivityResult == ConnectivityResult.ethernet) {
    print('이더넷에 연결되어 있습니다.');
  } else if (connectivityResult == ConnectivityResult.vpn) {
    print('VPN에 연결되어 있습니다.');
  } else if (connectivityResult == ConnectivityResult.bluetooth) {
    print('블루투스를 통해 연결되어 있습니다.');
  } else if (connectivityResult == ConnectivityResult.none) {
    print('네트워크에 연결되어 있지 않습니다.');
  } else {
    print('알 수 없는 연결 상태입니다.');
  }
}

1.2 연결 상태 스트림 모니터링하기

연결 상태 변화를 실시간으로 모니터링하려면 스트림을 사용합니다:

import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';

class NetworkMonitor {
  final Connectivity _connectivity = Connectivity();
  late StreamSubscription<ConnectivityResult> _subscription;

  void initialize() {
    // 초기 연결 상태 확인
    checkConnectivity();

    // 연결 상태 변화 모니터링
    _subscription = _connectivity.onConnectivityChanged.listen((ConnectivityResult result) {
      print('연결 상태가 변경되었습니다: $result');

      if (result == ConnectivityResult.none) {
        // 네트워크 연결이 끊겼을 때 로직
        showOfflineUI();
      } else {
        // 네트워크에 연결되었을 때 로직
        showOnlineUI();
        syncData(); // 오프라인 데이터 동기화 등
      }
    });
  }

  void dispose() {
    _subscription.cancel();
  }

  // UI 업데이트 및 기타 메서드...
  void showOfflineUI() {
    // 오프라인 UI 표시
  }

  void showOnlineUI() {
    // 온라인 UI 표시
  }

  void syncData() {
    // 데이터 동기화
  }
}

StatefulWidget에서 이를 활용하는 예시:

class _MyAppState extends State<MyApp> {
  final NetworkMonitor _networkMonitor = NetworkMonitor();
  bool _isConnected = true;

  @override
  void initState() {
    super.initState();
    _initConnectivity();
  }

  Future<void> _initConnectivity() async {
    var result = await Connectivity().checkConnectivity();
    setState(() {
      _isConnected = result != ConnectivityResult.none;
    });

    Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
      setState(() {
        _isConnected = result != ConnectivityResult.none;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('네트워크 상태 데모'),
        ),
        body: Center(
          child: _isConnected
            ? Text('온라인 상태입니다.')
            : Text('오프라인 상태입니다. 네트워크 연결을 확인해주세요.'),
        ),
      ),
    );
  }
}

2. 실제 인터넷 연결 확인하기

connectivity_plus는 네트워크 인터페이스의 연결 상태만 확인하고 실제 인터넷 연결 여부는 확인하지 않습니다. 실제로 인터넷에 접속 가능한지 확인하려면 HTTP 요청을 보내는 것이 좋습니다:

import 'package:http/http.dart' as http;

Future<bool> checkInternetConnection() async {
  try {
    final response = await http.get(Uri.parse('https://www.google.com'))
        .timeout(Duration(seconds: 5));
    return response.statusCode == 200;
  } catch (e) {
    return false;
  }
}

이 두 방법을 결합하여 더 신뢰성 있는 연결 확인이 가능합니다:

Future<bool> isInternetConnected() async {
  var connectivityResult = await Connectivity().checkConnectivity();
  if (connectivityResult == ConnectivityResult.none) {
    return false;
  } else {
    return await checkInternetConnection();
  }
}

3. internet_connection_checker 패키지 사용하기

더 강력한 인터넷 연결 확인을 위해 internet_connection_checker 패키지를 사용할 수 있습니다:

// pubspec.yaml
dependencies:
  internet_connection_checker: ^1.0.0
import 'package:internet_connection_checker/internet_connection_checker.dart';

Future<void> checkRealInternetConnection() async {
  bool result = await InternetConnectionChecker().hasConnection;

  if (result) {
    print('인터넷에 연결되어 있습니다.');
  } else {
    print('인터넷에 연결되어 있지 않습니다.');
    print(InternetConnectionChecker().lastTryResults);
  }
}

// 인터넷 연결 상태 리스너
void listenToInternetConnection() {
  InternetConnectionChecker().onStatusChange.listen((status) {
    switch (status) {
      case InternetConnectionStatus.connected:
        print('인터넷 연결이 복원되었습니다.');
        break;
      case InternetConnectionStatus.disconnected:
        print('인터넷 연결이 끊겼습니다.');
        break;
    }
  });
}

이 패키지는 여러 주소로 연결을 시도하므로 단일 서버 장애에 덜 취약합니다.

4. 연결 상태에 따른 UI 처리

네트워크 연결 상태에 따라 적절한 UI를 표시하는 것이 좋습니다:

class NetworkAwareWidget extends StatelessWidget {
  final Widget onlineChild;
  final Widget offlineChild;

  const NetworkAwareWidget({
    Key? key,
    required this.onlineChild,
    required this.offlineChild,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<ConnectivityResult>(
      stream: Connectivity().onConnectivityChanged,
      builder: (context, snapshot) {
        if (snapshot.data == ConnectivityResult.none) {
          return offlineChild;
        }
        return onlineChild;
      },
    );
  }
}

// 사용 예시
NetworkAwareWidget(
  onlineChild: HomeScreen(),
  offlineChild: OfflineScreen(),
)

5. Provider를 사용한 연결 상태 관리

상태 관리를 위해 Provider 패키지와 함께 연결 상태를 관리할 수 있습니다:

// pubspec.yaml
dependencies:
  provider: ^6.0.5
  connectivity_plus: ^4.0.2

// 연결 상태 제공자
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

class ConnectivityProvider with ChangeNotifier {
  bool _isOnline = true;
  bool get isOnline => _isOnline;

  ConnectivityProvider() {
    Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
      _isOnline = (result != ConnectivityResult.none);
      notifyListeners();
    });
  }

  Future<void> checkConnectivity() async {
    final result = await Connectivity().checkConnectivity();
    _isOnline = (result != ConnectivityResult.none);
    notifyListeners();
  }
}

// main.dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ConnectivityProvider(),
      child: MyApp(),
    ),
  );
}

// 사용 예시
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final isOnline = context.watch<ConnectivityProvider>().isOnline;

    return Scaffold(
      appBar: AppBar(title: Text('홈')),
      body: Center(
        child: isOnline
            ? Text('온라인 상태입니다')
            : Text('오프라인 상태입니다. 네트워크 연결을 확인해주세요.'),
      ),
    );
  }
}

6. 오프라인 지원 기능 구현하기

네트워크 연결이 없을 때도 앱이 계속 작동하도록 오프라인 지원 기능을 구현하는 것이 좋습니다:

6.1 오프라인 캐싱 (shared_preferences 사용)

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class OfflineCache {
  // 데이터 저장
  static Future<bool> saveData(String key, dynamic data) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.setString(key, json.encode(data));
  }

  // 데이터 로드
  static Future<dynamic> loadData(String key) async {
    final prefs = await SharedPreferences.getInstance();
    String? jsonString = prefs.getString(key);
    if (jsonString == null) return null;
    return json.decode(jsonString);
  }
}

// 사용 예시
Future<List<Product>> fetchProducts() async {
  // 인터넷 연결 확인
  bool isConnected = await isInternetConnected();

  if (isConnected) {
    try {
      // 온라인 데이터 가져오기
      final response = await http.get(Uri.parse('https://api.example.com/products'));
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        // 캐시에 저장
        await OfflineCache.saveData('products', data);
        return Product.listFromJson(data);
      } else {
        throw Exception('데이터 로드 실패');
      }
    } catch (e) {
      // 오류 발생 시 캐시 데이터 시도
      final cachedData = await OfflineCache.loadData('products');
      if (cachedData != null) {
        return Product.listFromJson(cachedData);
      }
      throw Exception('네트워크 오류: $e');
    }
  } else {
    // 오프라인 상태일 때 캐시 데이터
    final cachedData = await OfflineCache.loadData('products');
    if (cachedData != null) {
      return Product.listFromJson(cachedData);
    }
    throw Exception('오프라인 상태이며 캐시된 데이터가 없습니다.');
  }
}

6.2 오프라인 작업 큐 구현

네트워크가 끊겼을 때 사용자 작업을 저장했다가 연결이 복구되면 실행하는 큐를 구현할 수 있습니다:

class OfflineQueueManager {
  static List<Map<String, dynamic>> _pendingOperations = [];

  // 작업을 큐에 추가
  static void addOperation(String operation, Map<String, dynamic> data) {
    _pendingOperations.add({
      'operation': operation,
      'data': data,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
    });
    _savePendingOperations();
  }

  // 보류 중인 작업 저장
  static Future<void> _savePendingOperations() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('pending_operations', json.encode(_pendingOperations));
  }

  // 보류 중인 작업 로드
  static Future<void> loadPendingOperations() async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString('pending_operations');
    if (data != null) {
      _pendingOperations = List<Map<String, dynamic>>.from(
        json.decode(data).map((x) => Map<String, dynamic>.from(x))
      );
    }
  }

  // 연결이 복구되었을 때 보류 중인 작업 처리
  static Future<void> processQueue() async {
    await loadPendingOperations();

    if (_pendingOperations.isEmpty) return;

    final operations = List.from(_pendingOperations);
    _pendingOperations.clear();
    await _savePendingOperations();

    for (var op in operations) {
      await _processOperation(op);
    }
  }

  // 각 작업 처리 (실제 API 호출 등)
  static Future<void> _processOperation(Map<String, dynamic> operation) async {
    String opType = operation['operation'];
    Map<String, dynamic> data = operation['data'];

    try {
      switch (opType) {
        case 'create':
          await ApiClient.create(data);
          break;
        case 'update':
          await ApiClient.update(data['id'], data);
          break;
        case 'delete':
          await ApiClient.delete(data['id']);
          break;
        default:
          print('알 수 없는 작업 유형: $opType');
      }
    } catch (e) {
      print('작업 처리 중 오류: $e');
      // 실패한 작업 다시 큐에 추가
      _pendingOperations.add(operation);
      await _savePendingOperations();
    }
  }
}

// 사용 예시
void handleCreateItem(Item item) async {
  if (await isInternetConnected()) {
    await ApiClient.create(item.toJson());
  } else {
    // 오프라인 상태에서는 큐에 추가
    OfflineQueueManager.addOperation('create', item.toJson());
    showMessage('오프라인 모드: 네트워크 연결이 복원되면 변경 사항이 동기화됩니다.');
  }
}

// 네트워크 연결 복구 시 큐 처리
void onNetworkRestored() {
  OfflineQueueManager.processQueue();
}

7. Flutter Web에서의 연결 상태 확인

Flutter Web에서는 다른 접근 방식이 필요합니다:

import 'dart:html' as html;

bool isOnlineWeb() {
  return html.window.navigator.onLine ?? true;
}

void listenOnlineStatusWeb() {
  html.window.onOnline.listen((_) {
    print('온라인 상태로 전환되었습니다.');
  });

  html.window.onOffline.listen((_) {
    print('오프라인 상태로 전환되었습니다.');
  });
}

8. 플랫폼 별 코드 처리하기

다양한 플랫폼을 지원하는 앱에서는 플랫폼별로 다른 구현이 필요할 수 있습니다:

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;

Future<bool> isConnected() async {
  if (kIsWeb) {
    // 웹 플랫폼용 구현
    return html.window.navigator.onLine ?? true;
  } else if (Platform.isAndroid || Platform.isIOS) {
    // 모바일 플랫폼용 구현
    return await InternetConnectionChecker().hasConnection;
  } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
    // 데스크톱 플랫폼용 구현
    try {
      final result = await InternetAddress.lookup('google.com');
      return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
    } catch (_) {
      return false;
    }
  }
  return false;
}

9. 연결 상태에 따른 에러 처리

API 호출 시 네트워크 상태에 따른 에러 처리도 중요합니다:

Future<T> networkSafeApiCall<T>(Future<T> Function() apiCall) async {
  try {
    // 인터넷 연결 먼저 확인
    if (!await isInternetConnected()) {
      throw NetworkException('인터넷 연결이 없습니다.');
    }

    // API 호출 시도
    return await apiCall();
  } on SocketException {
    throw NetworkException('서버에 연결할 수 없습니다. 네트워크 연결을 확인하세요.');
  } on TimeoutException {
    throw NetworkException('요청 시간이 초과되었습니다.');
  } on http.ClientException {
    throw NetworkException('HTTP 클라이언트 오류가 발생했습니다.');
  } catch (e) {
    throw NetworkException('알 수 없는 오류: $e');
  }
}

// 사용 예시
Future<List<Product>> getProducts() async {
  return networkSafeApiCall(() async {
    final response = await http.get(
      Uri.parse('https://api.example.com/products'),
      headers: {'Authorization': 'Bearer $token'},
    ).timeout(Duration(seconds: 10));

    if (response.statusCode == 200) {
      return Product.listFromJson(json.decode(response.body));
    } else {
      throw ApiException('Products fetch failed: ${response.statusCode}');
    }
  });
}

10. 자동 재시도 메커니즘 구현

네트워크 오류 발생 시 자동으로 재시도하는 메커니즘을 구현할 수 있습니다:

Future<T> retryApi<T>(
  Future<T> Function() apiCall, {
  int maxRetries = 3,
  Duration delayBetweenRetries = const Duration(seconds: 2),
}) async {
  int retries = 0;
  while (true) {
    try {
      return await apiCall();
    } catch (e) {
      retries++;
      if (retries >= maxRetries) {
        rethrow;
      }
      print('API 호출 실패. $retries번째 재시도 중... 오류: $e');
      await Future.delayed(delayBetweenRetries);
    }
  }
}

// 사용 예시
Future<List<Product>> fetchProductsWithRetry() async {
  return retryApi(
    () => getProducts(),
    maxRetries: 3,
    delayBetweenRetries: Duration(seconds: 2),
  );
}

요약

Flutter 앱에서 인터넷 연결 상태를 확인하고 관리하는 방법은 다음과 같이 요약할 수 있습니다:

  1. 연결 상태 확인: connectivity_plus 패키지를 사용하여 현재 네트워크 연결 유형을 확인합니다.

  2. 실제 인터넷 확인: 네트워크 인터페이스 연결이 있더라도 실제 인터넷 접속이 가능한지 확인하기 위해 HTTP 요청이나 internet_connection_checker 패키지를 사용합니다.

  3. 연결 상태 모니터링: 스트림을 사용하여 연결 상태 변화를 실시간으로 감지하고 적절히 대응합니다.

  4. UI 처리: 연결 상태에 따라 다른 UI를 표시하여 사용자에게 피드백을 제공합니다.

  5. 오프라인 지원: 캐싱, 오프라인 작업 큐 등을 구현하여 네트워크 연결이 없을 때도 앱이 작동하도록 합니다.

  6. 에러 처리: 네트워크 문제로 인한 예외를 적절히 처리하고 사용자에게 의미 있는 피드백을 제공합니다.

효과적인 네트워크 연결 관리는 앱의 안정성과 사용자 경험을 크게 향상시킬 수 있으며, 특히 네트워크 환경이 불안정한 상황에서 더욱 중요합니다.

results matching ""

    No results matching ""