Flutter에서 네트워크 통신은 어떻게 구현하나요?

질문

Flutter에서 API 통신 및 네트워크 요청을 처리하는 다양한 방법과 라이브러리에 대해 설명해주세요.

답변

Flutter에서 네트워크 통신을 구현하는 방법은 여러 가지가 있으며, 각각의 접근 방식은 프로젝트 요구사항에 따라 선택할 수 있습니다. 기본적인 HTTP 요청부터 고급 REST API 클라이언트까지 다양한 방법을 살펴보겠습니다.

1. http 패키지

Flutter에서 가장 기본적인 네트워크 통신은 공식 http 패키지를 사용하는 것입니다.

설치 방법

# pubspec.yaml
dependencies:
  http: ^1.1.0

기본 사용법

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

Future<void> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));

    if (response.statusCode == 200) {
      // 성공적인 응답 처리
      final data = jsonDecode(response.body);
      print('데이터: $data');
    } else {
      // 오류 응답 처리
      print('요청 실패: ${response.statusCode}');
    }
  } catch (e) {
    // 예외 처리
    print('오류 발생: $e');
  }
}

// GET 요청 예시
Future<Map<String, dynamic>> getUser(int userId) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users/$userId'),
    headers: {'Authorization': 'Bearer your_token_here'},
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to load user');
  }
}

// POST 요청 예시
Future<void> createUser(String name, String email) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/users'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your_token_here',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
    }),
  );

  if (response.statusCode == 201) {
    print('사용자 생성 성공: ${response.body}');
  } else {
    throw Exception('Failed to create user');
  }
}

2. dio 패키지

dio는 HTTP 요청에 더 많은 기능을 제공하는 강력한 패키지입니다. 인터셉터, 글로벌 설정, 요청 취소, 파일 다운로드 등이 포함됩니다.

설치 방법

# pubspec.yaml
dependencies:
  dio: ^5.3.2

기본 사용법

import 'package:dio/dio.dart';

final dio = Dio();

// 기본 설정
void configureDio() {
  dio.options.baseUrl = 'https://api.example.com';
  dio.options.connectTimeout = Duration(seconds: 5);
  dio.options.receiveTimeout = Duration(seconds: 3);
  dio.options.headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  };

  // 인터셉터 추가
  dio.interceptors.add(LogInterceptor(responseBody: true));
}

// GET 요청
Future<void> getUsers() async {
  try {
    final response = await dio.get('/users',
      queryParameters: {'page': 1},
      options: Options(
        headers: {'Authorization': 'Bearer your_token'}
      )
    );

    print('사용자 목록: ${response.data}');
  } on DioException catch (e) {
    if (e.response != null) {
      print('Dio 오류: ${e.response!.statusCode} - ${e.response!.statusMessage}');
    } else {
      print('오류 발생: ${e.message}');
    }
  }
}

// POST 요청
Future<Response> createUser(Map<String, dynamic> userData) async {
  try {
    return await dio.post('/users', data: userData);
  } catch (e) {
    rethrow;
  }
}

// 파일 업로드
Future<void> uploadFile(String filePath) async {
  FormData formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(filePath, filename: 'image.jpg'),
    'description': '프로필 이미지'
  });

  try {
    final response = await dio.post('/upload', data: formData);
    print('업로드 성공: ${response.data}');
  } catch (e) {
    print('업로드 실패: $e');
  }
}

3. Retrofit

Retrofit은 REST API 호출을 위한 타입 안전한 클라이언트를 생성하는 패키지로, 어노테이션을 사용하여 HTTP 요청을 정의합니다.

설치 방법

# pubspec.yaml
dependencies:
  retrofit: ^4.0.1
  dio: ^5.3.2
  json_annotation: ^4.8.1

dev_dependencies:
  retrofit_generator: ^7.0.8
  build_runner: ^2.4.6
  json_serializable: ^6.7.1

사용 예시

// api_client.dart
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:json_annotation/json_annotation.dart';

part 'api_client.g.dart'; // build_runner로 생성될 파일

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

@RestApi(baseUrl: "https://api.example.com")
abstract class ApiClient {
  factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;

  @GET("/users")
  Future<List<User>> getUsers();

  @GET("/users/{id}")
  Future<User> getUser(@Path("id") int id);

  @POST("/users")
  Future<User> createUser(@Body() User user);

  @PUT("/users/{id}")
  Future<User> updateUser(@Path("id") int id, @Body() User user);

  @DELETE("/users/{id}")
  Future<void> deleteUser(@Path("id") int id);
}

// 사용 방법
final dio = Dio();
final client = ApiClient(dio);

Future<void> fetchData() async {
  try {
    final users = await client.getUsers();
    print('사용자 목록: $users');

    final user = await client.getUser(1);
    print('사용자 정보: $user');
  } catch (e) {
    print('오류 발생: $e');
  }
}

이 코드를 사용하기 전에 다음 명령어로 필요한 파일을 생성해야 합니다:

flutter pub run build_runner build

4. GraphQL

GraphQL API와 통신해야 하는 경우 graphql_flutter 패키지를 사용할 수 있습니다.

설치 방법

# pubspec.yaml
dependencies:
  graphql_flutter: ^5.1.2

기본 사용법

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() async {
  // GraphQL 클라이언트 초기화
  final HttpLink httpLink = HttpLink('https://api.github.com/graphql');

  final AuthLink authLink = AuthLink(
    getToken: () => 'Bearer YOUR_GITHUB_PERSONAL_ACCESS_TOKEN',
  );

  final Link link = authLink.concat(httpLink);

  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: link,
      cache: GraphQLCache(store: HiveStore()),
    ),
  );

  runApp(
    GraphQLProvider(
      client: client,
      child: MyApp(),
    ),
  );
}

// Query 사용 예시
class RepositoryListScreen extends StatelessWidget {
  final String readRepositories = """
  query ReadRepositories(\$nRepositories: Int!) {
    viewer {
      repositories(last: \$nRepositories) {
        nodes {
          id
          name
          viewerHasStarred
        }
      }
    }
  }
  """;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GitHub 리포지토리')),
      body: Query(
        options: QueryOptions(
          document: gql(readRepositories),
          variables: {'nRepositories': 10},
        ),
        builder: (QueryResult result, {refetch, fetchMore}) {
          if (result.hasException) {
            return Center(child: Text('오류 발생: ${result.exception.toString()}'));
          }

          if (result.isLoading) {
            return Center(child: CircularProgressIndicator());
          }

          final repositories = result.data!['viewer']['repositories']['nodes'] as List;

          return ListView.builder(
            itemCount: repositories.length,
            itemBuilder: (context, index) {
              final repository = repositories[index];
              return ListTile(
                title: Text(repository['name']),
                trailing: repository['viewerHasStarred']
                  ? Icon(Icons.star, color: Colors.yellow)
                  : Icon(Icons.star_border),
              );
            },
          );
        },
      ),
    );
  }
}

5. Provider 패턴을 활용한 API 서비스

대규모 앱에서는 API 통신을 서비스 클래스로 분리하고 Provider 패턴으로 관리하는 것이 좋습니다.

// api_service.dart
import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio = Dio();

  ApiService() {
    _dio.options.baseUrl = 'https://api.example.com';
    _dio.options.headers = {'Content-Type': 'application/json'};
  }

  Future<List<dynamic>> getUsers() async {
    try {
      final response = await _dio.get('/users');
      return response.data;
    } catch (e) {
      throw Exception('Failed to get users: $e');
    }
  }

  Future<Map<String, dynamic>> getUser(int id) async {
    try {
      final response = await _dio.get('/users/$id');
      return response.data;
    } catch (e) {
      throw Exception('Failed to get user: $e');
    }
  }
}

// user_provider.dart
import 'package:flutter/foundation.dart';
import 'api_service.dart';

class UserProvider extends ChangeNotifier {
  final ApiService _apiService = ApiService();

  List<dynamic> _users = [];
  Map<String, dynamic>? _selectedUser;
  bool _isLoading = false;
  String? _error;

  List<dynamic> get users => _users;
  Map<String, dynamic>? get selectedUser => _selectedUser;
  bool get isLoading => _isLoading;
  String? get error => _error;

  Future<void> loadUsers() async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _users = await _apiService.getUsers();
      _isLoading = false;
      notifyListeners();
    } catch (e) {
      _isLoading = false;
      _error = e.toString();
      notifyListeners();
    }
  }

  Future<void> loadUser(int id) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _selectedUser = await _apiService.getUser(id);
      _isLoading = false;
      notifyListeners();
    } catch (e) {
      _isLoading = false;
      _error = e.toString();
      notifyListeners();
    }
  }
}

6. 네트워크 통신 관련 고려사항

6.1 인터넷 연결 확인

네트워크 요청 전에 인터넷 연결을 확인하는 것이 좋습니다. connectivity_plus 패키지를 사용할 수 있습니다:

import 'package:connectivity_plus/connectivity_plus.dart';

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

Future<void> fetchData() async {
  if (await isInternetConnected()) {
    // 데이터 가져오기
  } else {
    // 인터넷 연결 없음 처리
  }
}

6.2 인증 토큰 관리

JWT와 같은 인증 토큰을 안전하게 관리해야 합니다:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final storage = FlutterSecureStorage();

  Future<void> saveToken(String token) async {
    await storage.write(key: 'auth_token', value: token);
  }

  Future<String?> getToken() async {
    return await storage.read(key: 'auth_token');
  }

  Future<void> deleteToken() async {
    await storage.delete(key: 'auth_token');
  }
}

6.3 응답 캐싱

오프라인 지원 또는 성능 향상을 위해 응답을 캐싱할 수 있습니다:

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

class ApiCache {
  Future<void> cacheData(String key, dynamic data) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, jsonEncode(data));
    await prefs.setInt('${key}_timestamp', DateTime.now().millisecondsSinceEpoch);
  }

  Future<dynamic> getCachedData(String key, {int maxAgeMinutes = 60}) async {
    final prefs = await SharedPreferences.getInstance();
    final cachedData = prefs.getString(key);
    final timestamp = prefs.getInt('${key}_timestamp') ?? 0;

    if (cachedData != null) {
      final age = DateTime.now().millisecondsSinceEpoch - timestamp;
      if (age < maxAgeMinutes * 60 * 1000) {
        return jsonDecode(cachedData);
      }
    }

    return null; // 캐시 없음 또는 만료됨
  }
}

결론

Flutter에서 네트워크 통신을 구현하는 방법은 다양하며, 프로젝트의 요구사항에 맞는 접근 방식을 선택해야 합니다:

  1. 간단한 API 통신: http 패키지로 충분
  2. 더 많은 기능이 필요한 경우: dio 패키지 사용
  3. 타입 안전한 API 클라이언트: Retrofit 활용
  4. GraphQL API 통신: graphql_flutter 패키지 사용
  5. 큰 규모의 앱: Provider 패턴과 함께 API 서비스 클래스 구현

어떤 방법을 선택하든, 오류 처리, 로딩 상태 관리, 인터넷 연결 확인 등의 기본적인 고려사항을 항상 염두에 두어야 합니다. 또한 적절한 캐싱 전략을 통해 앱의 성능과 오프라인 경험을 향상시키는 것이 좋습니다.

results matching ""

    No results matching ""