네트워크 호출을 위한 http 패키지 사용에 대해 설명해주세요

질문

Flutter에서 네트워크 통신을 위한 http 패키지 사용방법과 RESTful API 호출 구현 방법에 대해 설명해주세요.

답변

Flutter에서 HTTP 요청을 처리하는 가장 기본적인 방법은 http 패키지를 사용하는 것입니다. 이 패키지는 Flutter 팀에서 공식적으로 관리하며, RESTful API와 통신하기 위한 간단하면서도 강력한 인터페이스를 제공합니다.

1. http 패키지 설치

먼저, pubspec.yaml 파일에 http 패키지를 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0 # 최신 버전 사용 권장

그리고 터미널에서 패키지를 가져오기 위해 다음 명령을 실행합니다:

flutter pub get

2. 기본 HTTP 요청

2.1 GET 요청

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

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

    // 상태 코드 확인
    if (response.statusCode == 200) {
      // 응답 데이터 파싱
      final Map<String, dynamic> data = json.decode(response.body);
      print('데이터: $data');
    } else {
      print('요청 실패: ${response.statusCode}');
      print('응답: ${response.body}');
    }
  } catch (e) {
    print('오류 발생: $e');
  }
}

2.2 POST 요청

Future<void> createUser(String name, String email) async {
  try {
    final response = await http.post(
      Uri.parse('https://api.example.com/users'),
      headers: {
        'Content-Type': 'application/json',
      },
      body: json.encode({
        'name': name,
        'email': email,
      }),
    );

    if (response.statusCode == 201) {
      final Map<String, dynamic> data = json.decode(response.body);
      print('생성된 사용자: $data');
    } else {
      print('요청 실패: ${response.statusCode}');
    }
  } catch (e) {
    print('오류 발생: $e');
  }
}

2.3 PUT 요청

Future<void> updateUser(int id, String name) async {
  final response = await http.put(
    Uri.parse('https://api.example.com/users/$id'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'name': name}),
  );

  if (response.statusCode == 200) {
    print('사용자 업데이트 성공');
  } else {
    print('사용자 업데이트 실패: ${response.statusCode}');
  }
}

2.4 DELETE 요청

Future<void> deleteUser(int id) async {
  final response = await http.delete(
    Uri.parse('https://api.example.com/users/$id'),
  );

  if (response.statusCode == 204) {
    print('사용자 삭제 성공');
  } else {
    print('사용자 삭제 실패: ${response.statusCode}');
  }
}

3. 요청 헤더 추가

인증 토큰, 콘텐츠 타입 등의 헤더를 추가할 수 있습니다:

Future<void> fetchUserData(String token) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/user'),
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  );

  // 응답 처리
}

4. 쿼리 파라미터 사용

Future<void> searchProducts(String query, int page) async {
  final uri = Uri.parse('https://api.example.com/products')
      .replace(queryParameters: {
    'q': query,
    'page': page.toString(),
    'limit': '20',
  });

  final response = await http.get(uri);

  // 응답 처리
}

5. 타임아웃 설정

기본적으로 http 패키지는 요청 타임아웃이 없습니다. 타임아웃을 설정하려면:

Future<void> fetchWithTimeout() async {
  try {
    final client = http.Client();
    final request = http.Request('GET', Uri.parse('https://api.example.com/data'));

    final response = await client.send(request).timeout(
      Duration(seconds: 10),
      onTimeout: () {
        client.close();
        throw TimeoutException('요청 시간 초과');
      },
    );

    final responseBody = await response.stream.bytesToString();

    // 응답 처리
    print(responseBody);

    client.close();
  } catch (e) {
    print('오류: $e');
  }
}

6. JSON 응답 파싱

응답 데이터를 모델 클래스로 변환하는 패턴:

// 모델 클래스 정의
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) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

// API 요청 및 모델 변환
Future<List<User>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));

  if (response.statusCode == 200) {
    final List<dynamic> usersJson = json.decode(response.body);
    return usersJson.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users');
  }
}

7. 에러 처리

네트워크 요청은 다양한 오류가 발생할 수 있으므로 적절한 예외 처리가 필요합니다:

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

    // HTTP 상태 코드 확인
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return json.decode(response.body);
    } else if (response.statusCode == 401) {
      throw Exception('인증 실패');
    } else if (response.statusCode == 404) {
      throw Exception('리소스를 찾을 수 없음');
    } else {
      throw Exception('서버 오류: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('네트워크 연결 없음');
  } on TimeoutException {
    throw Exception('요청 시간 초과');
  } on FormatException {
    throw Exception('잘못된 응답 형식');
  } catch (e) {
    throw Exception('알 수 없는 오류: $e');
  }
}

// 사용 예시
void fetchDataAndHandle() async {
  try {
    final data = await fetchData();
    print('데이터: $data');
  } catch (e) {
    print('오류: $e');
    // 오류에 따른 UI 표시
  }
}

8. HTTP 클라이언트 커스터마이징

기본 http 클라이언트를 확장하여 공통 기능을 구현할 수 있습니다:

class ApiClient {
  final http.Client _client = http.Client();
  final String _baseUrl;
  final Map<String, String> _defaultHeaders;

  ApiClient({
    required String baseUrl,
    Map<String, String>? defaultHeaders,
  }) : _baseUrl = baseUrl,
       _defaultHeaders = defaultHeaders ?? {};

  // 인증 토큰 설정
  void setAuthToken(String token) {
    _defaultHeaders['Authorization'] = 'Bearer $token';
  }

  Future<dynamic> get(String path, {Map<String, String>? headers}) async {
    final response = await _client.get(
      Uri.parse('$_baseUrl$path'),
      headers: {..._defaultHeaders, ...?headers},
    );
    return _handleResponse(response);
  }

  Future<dynamic> post(
    String path,
    {Map<String, String>? headers, dynamic body}
  ) async {
    final response = await _client.post(
      Uri.parse('$_baseUrl$path'),
      headers: {
        'Content-Type': 'application/json',
        ..._defaultHeaders,
        ...?headers
      },
      body: json.encode(body),
    );
    return _handleResponse(response);
  }

  // PUT, DELETE 등 다른 메소드 추가

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      if (response.body.isEmpty) return null;
      return json.decode(response.body);
    } else {
      throw HttpException(
        response.body,
        uri: response.request?.url,
        statusCode: response.statusCode,
      );
    }
  }

  void close() {
    _client.close();
  }
}

// 사용 예시
final apiClient = ApiClient(baseUrl: 'https://api.example.com');
apiClient.setAuthToken('your-auth-token');

// API 호출
Future<void> fetchUserProfile() async {
  try {
    final userData = await apiClient.get('/user/profile');
    print('사용자 데이터: $userData');
  } catch (e) {
    print('오류: $e');
  }
}

9. 멀티파트 요청과 파일 업로드

이미지나 파일을 업로드하기 위한 멀티파트 요청:

Future<void> uploadImage(File imageFile) async {
  final uri = Uri.parse('https://api.example.com/upload');

  // 멀티파트 요청 생성
  final request = http.MultipartRequest('POST', uri);

  // 헤더 추가
  request.headers['Authorization'] = 'Bearer your-token';

  // 파일 첨부
  request.files.add(await http.MultipartFile.fromPath(
    'image',  // 서버에서 기대하는 필드 이름
    imageFile.path,
  ));

  // 추가 필드 첨부
  request.fields['description'] = '이미지 설명';

  // 요청 전송
  final response = await request.send();

  // 응답 처리
  if (response.statusCode == 200) {
    final responseBody = await response.stream.bytesToString();
    print('업로드 성공: $responseBody');
  } else {
    print('업로드 실패: ${response.statusCode}');
  }
}

10. 동시 요청 처리

여러 HTTP 요청을 동시에 처리하기:

Future<void> fetchMultipleResources() async {
  try {
    // 여러 API 요청을 동시에 실행
    final results = await Future.wait([
      http.get(Uri.parse('https://api.example.com/users')),
      http.get(Uri.parse('https://api.example.com/products')),
      http.get(Uri.parse('https://api.example.com/categories')),
    ]);

    // 각 응답 처리
    final usersResponse = results[0];
    final productsResponse = results[1];
    final categoriesResponse = results[2];

    if (usersResponse.statusCode == 200) {
      final users = json.decode(usersResponse.body);
      print('사용자: $users');
    }

    if (productsResponse.statusCode == 200) {
      final products = json.decode(productsResponse.body);
      print('제품: $products');
    }

    if (categoriesResponse.statusCode == 200) {
      final categories = json.decode(categoriesResponse.body);
      print('카테고리: $categories');
    }
  } catch (e) {
    print('오류 발생: $e');
  }
}

11. RESTful API 통합 예제

실제 애플리케이션에서 http 패키지를 사용한 RESTful API 통합 예제:

// API 서비스 클래스
class TodoService {
  final String baseUrl = 'https://jsonplaceholder.typicode.com';
  final http.Client client = http.Client();

  Future<List<Todo>> getTodos() async {
    final response = await client.get(Uri.parse('$baseUrl/todos'));

    if (response.statusCode == 200) {
      final List<dynamic> data = json.decode(response.body);
      return data.map((json) => Todo.fromJson(json)).toList();
    } else {
      throw Exception('할 일 목록을 가져오는데 실패했습니다');
    }
  }

  Future<Todo> getTodoById(int id) async {
    final response = await client.get(Uri.parse('$baseUrl/todos/$id'));

    if (response.statusCode == 200) {
      return Todo.fromJson(json.decode(response.body));
    } else {
      throw Exception('할 일을 가져오는데 실패했습니다');
    }
  }

  Future<Todo> createTodo(Todo todo) async {
    final response = await client.post(
      Uri.parse('$baseUrl/todos'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode(todo.toJson()),
    );

    if (response.statusCode == 201) {
      return Todo.fromJson(json.decode(response.body));
    } else {
      throw Exception('할 일을 생성하는데 실패했습니다');
    }
  }

  Future<Todo> updateTodo(int id, Todo todo) async {
    final response = await client.put(
      Uri.parse('$baseUrl/todos/$id'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode(todo.toJson()),
    );

    if (response.statusCode == 200) {
      return Todo.fromJson(json.decode(response.body));
    } else {
      throw Exception('할 일을 업데이트하는데 실패했습니다');
    }
  }

  Future<void> deleteTodo(int id) async {
    final response = await client.delete(Uri.parse('$baseUrl/todos/$id'));

    if (response.statusCode != 200) {
      throw Exception('할 일을 삭제하는데 실패했습니다');
    }
  }

  void dispose() {
    client.close();
  }
}

// Todo 모델 클래스
class Todo {
  final int? id;
  final String title;
  final bool completed;
  final int userId;

  Todo({
    this.id,
    required this.title,
    required this.completed,
    required this.userId,
  });

  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      id: json['id'],
      title: json['title'],
      completed: json['completed'],
      userId: json['userId'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'completed': completed,
      'userId': userId,
    };
  }
}

// UI에서 사용 예시
class TodoListScreen extends StatefulWidget {
  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  final TodoService _todoService = TodoService();
  late Future<List<Todo>> _todosFuture;

  @override
  void initState() {
    super.initState();
    _todosFuture = _todoService.getTodos();
  }

  @override
  void dispose() {
    _todoService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('할 일 목록')),
      body: FutureBuilder<List<Todo>>(
        future: _todosFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('오류: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('할 일이 없습니다'));
          } else {
            final todos = snapshot.data!;
            return ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                final todo = todos[index];
                return ListTile(
                  title: Text(todo.title),
                  leading: Checkbox(
                    value: todo.completed,
                    onChanged: null,
                  ),
                  trailing: IconButton(
                    icon: Icon(Icons.delete),
                    onPressed: () async {
                      try {
                        await _todoService.deleteTodo(todo.id!);
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('할 일이 삭제되었습니다')),
                        );
                        // 목록 갱신
                        setState(() {
                          _todosFuture = _todoService.getTodos();
                        });
                      } catch (e) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('삭제 실패: $e')),
                        );
                      }
                    },
                  ),
                );
              },
            );
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          // 새 할 일 추가 화면으로 이동
        },
      ),
    );
  }
}

12. http vs dio 패키지 비교

Flutter에서 HTTP 통신에 자주 사용되는 두 패키지의 특징 비교:

http 패키지:

  • Flutter 팀에서 공식 관리
  • 간단한 API 구조로 쉽게 배울 수 있음
  • 기본적인 HTTP 기능만 제공
  • 기본 타임아웃 설정 없음 (직접 구현 필요)
  • 인터셉터 및 미들웨어 미지원
  • 적은 코드 크기와 의존성

dio 패키지:

  • 더 많은 기능 제공 (인터셉터, 글로벌 설정, 요청 취소, 진행 상태 모니터링)
  • 내장 타임아웃 설정 지원
  • FormData 및 파일 업로드 편의 기능
  • 응답 변환 및 인터셉터 지원
  • HTTP/2 지원 (옵션)
  • 단일 인스턴스로 설정 공유 가능

간단한 애플리케이션은 http 패키지만으로 충분할 수 있지만, 더 복잡한 요구사항이 있다면 dio를 고려해볼 수 있습니다.

결론

http 패키지는 Flutter 앱에서 RESTful API와 통신하기 위한 가장 기본적이고 간단한 방법을 제공합니다. 기본적인 HTTP 메서드(GET, POST, PUT, DELETE)를 지원하며, 헤더 설정, 쿼리 매개변수 추가, 요청 본문 전송 등의 기능을 제공합니다.

복잡한 애플리케이션의 경우, 커스텀 클라이언트를 구현하거나 인터셉터와 같은 추가 기능이 필요하다면 dio 패키지를 고려할 수 있습니다. 하지만 대부분의 중소 규모 애플리케이션에서는 http 패키지만으로도 충분히 기능을 구현할 수 있습니다.

적절한 오류 처리, 타임아웃 설정, 모델 변환을 포함하여 http 패키지를 효과적으로 사용하면 견고한 네트워크 통신 레이어를 구축할 수 있습니다.

results matching ""

    No results matching ""