Search
📚

EP19. API Response object mapping

생성일
2024/06/09 13:18
마지막 업데이트
2024/06/16

EP19. API response object mapping

API Response 형태는 json으로 되어있습니다. Flutter에서 json은 결국 string 타입으로 API Response를 활용하기 위해서는 Flutter의 Object로 변환/매핑하는 과정이 필요합니다.
ex) Response
{ "code": 10000, "message": "Success", "result": { "fcmTokens": [ "token_1", "token_2", "token_3", "token_4" ] } }
JSON
복사

1. 공통 API Response Format Class 구현

패키지 lib/network디렉토리 하위에 response_format.dart파일을 추가합니다.
/// API 응답 형식을 정의하는 클래스. /// 제네릭 타입 T는 JSON으로부터 파싱될 객체 타입을 나타냅니다. class ResponseFormat<T> { /// 응답 코드. final int code; /// 응답 메시지. final String message; /// 응답 결과. 제네릭 타입 T 또는 null일 수 있습니다. final T? result; /// 생성자. ResponseFormat({ required this.code, required this.message, this.result, }); /// JSON으로부터 ResponseFormat 객체를 생성하는 팩토리 생성자. /// [json]은 응답 JSON 객체이며, [fromJsonT]는 제네릭 타입 T를 파싱하는 함수입니다. factory ResponseFormat.fromJson(Map<String, dynamic> json, T Function(Map<String, dynamic>) fromJsonT) { T? parsedResult; if (json['result'] != null) { parsedResult = fromJsonT(json['result']); } else { parsedResult = null; } return ResponseFormat( code: json['code'], message: json['message'], result: parsedResult, ); } } /// JSON 응답에서 결과가 없는 경우를 나타내는 클래스. class EmptyResult { /// 생성자. EmptyResult(); /// JSON으로부터 EmptyResult 객체를 생성하는 팩토리 생성자. /// [json]은 응답 JSON 객체입니다. factory EmptyResult.fromJson(Map<String, dynamic> json) { return EmptyResult(); } }
Dart
복사

2. APIClient 파싱 로직 구현

import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:swm_flutter_package/network/response_format.dart'; import 'package:swm_flutter_package/services/auth_service.dart'; class ApiClient { @@ -21,46 +22,47 @@ class ApiClient { // GET 요청을 처리하는 메서드입니다. // 지정된 endpoint로 GET 요청을 보내고 응답을 처리합니다. - Future<String> get(String endpoint) async { + Future<ResponseFormat<T>> get<T>(String endpoint, T Function(Map<String, dynamic>) fromJsonT) async { final headers = _getHeaders(); final response = await http.get( Uri.parse('$baseUrl$endpoint'), headers: headers, ); - return _handleResponse(response); + return _handleResponse<T>(response, fromJsonT); } // POST 요청을 처리하는 메서드입니다. // 지정된 endpoint로 POST 요청을 보내고 응답을 처리합니다. - Future<String> post(String endpoint, Object? body) async { + Future<ResponseFormat<T>> post<T>(String endpoint, Object? body, T Function(Map<String, dynamic>) fromJsonT) async { final headers = _getHeaders(); final response = await http.post( Uri.parse('$baseUrl$endpoint'), headers: headers, body: jsonEncode(body), ); - return _handleResponse(response); + return _handleResponse<T>(response, fromJsonT); } // DELETE 요청을 처리하는 메서드입니다. // 지정된 endpoint로 DELETE 요청을 보내고 응답을 처리합니다. - Future<String> delete(String endpoint, Object? body) async { + Future<ResponseFormat<T>> delete<T>(String endpoint, Object? body, T Function(Map<String, dynamic>) fromJsonT) async { final headers = _getHeaders(); final response = await http.delete( Uri.parse('$baseUrl$endpoint'), headers: headers, body: jsonEncode(body), ); - return _handleResponse(response); + return _handleResponse<T>(response, fromJsonT); } // HTTP 응답을 처리하는 메서드입니다. // 상태 코드가 200-299일 경우 응답 본문을 반환하고, 그렇지 않으면 예외를 발생시킵니다. - String _handleResponse(http.Response response) { + ResponseFormat<T> _handleResponse<T>(http.Response response, T Function(Map<String, dynamic>) fromJsonT) { if (response.statusCode >= 200 && response.statusCode < 300) { - return response.body; + final jsonResponse = jsonDecode(response.body) as Map<String, dynamic>; + return ResponseFormat<T>.fromJson(jsonResponse, fromJsonT); } else { - throw Exception('Failed to load data: ${response.statusCode}'); + return ResponseFormat(code: -999, message: "Parsing Error", result: null); } } }
Diff
복사
fromJson 생성자를 파라미터로 전달받아 사용하도록 변경합니다.
전체 코드 첨부
api_client.dart
network

3. APIClient를 사용하는 곳에서의 Result 타입 전달

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_package_sample/views/home/home_view_model.dart'; import 'package:provider/provider.dart'; import 'package:swm_flutter_package/network/api_client.dart'; +import 'package:swm_flutter_package/network/response_format.dart'; class HomeView extends StatelessWidget { HomeView({super.key}); @@ -55,7 +56,7 @@ class HomeView extends StatelessWidget { ElevatedButton( onPressed: () { // '추가' 버튼을 눌렀을 때 API의 POST 엔드포인트를 호출 - apiClient.post('/users/v1/fcmToken', {'fcmToken': textController.text}) + apiClient.post<EmptyResult>('/users/v1/fcmToken', {'fcmToken': textController.text}, (json) => EmptyResult.fromJson(json)) .then((value) => print(value)) // 응답을 출력 .catchError((error) => print(error)); // 에러를 출력 }, @@ -65,7 +66,7 @@ class HomeView extends StatelessWidget { ElevatedButton( onPressed: () { // '삭제' 버튼을 눌렀을 때 API의 DELETE 엔드포인트를 호출 - apiClient.delete('/users/v1/fcmToken', {'fcmToken': textController.text}) + apiClient.delete<EmptyResult>('/users/v1/fcmToken', {'fcmToken': textController.text}, (json) => EmptyResult.fromJson(json)) .then((value) => print(value)) // 응답을 출력 .catchError((error) => print(error)); // 에러를 출력 }, @@ -75,8 +76,8 @@ class HomeView extends StatelessWidget { ElevatedButton( onPressed: () { // '조회' 버튼을 눌렀을 때 API의 GET 엔드포인트를 호출 - apiClient.get('/users/v1/fcmToken') - .then((value) => print(value)) // 응답을 출력 + apiClient.get<FCMTokensResult>('/users/v1/fcmToken', (json) => FCMTokensResult.fromJson(json)) + .then((value) => print(value.result?.tokens)) // 응답을 출력 .catchError((error) => print(error)); // 에러를 출력 }, child: Text('조회'), @@ -88,4 +89,14 @@ class HomeView extends StatelessWidget { ), ); } +} + +class FCMTokensResult { + final List<String> tokens; + + FCMTokensResult({required this.tokens}); + + factory FCMTokensResult.fromJson(Map<String, dynamic> json) { + return FCMTokensResult(tokens: List<String>.from(json['fcmTokens'])); + } }
Diff
복사
APIClient를 활용해 request를 만들때 Result타입을 제네릭으로 전달합니다.
전달한 타입이 Response Result 타입으로 적용되어 반환됩니다.
전체 코드 첨부

강의 코드