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 ํƒ€์ž…์œผ๋กœ ์ ์šฉ๋˜์–ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
โ€ข
์ „์ฒด ์ฝ”๋“œ ์ฒจ๋ถ€

๊ฐ•์˜ ์ฝ”๋“œ