해당 게시글은 Udemy 'Learn Flutter MVVM Architecture | Build Weather App' 강의와 dart 공식 문서를 참고하였습니다.
https://dart-ko.dev/codelabs/async-await
다트 언어에서는 프로그램 하나당 스레드 하나가 사용된다.
그 말은 만약 작업이 오래 걸리는 프로세스를 실행하면 그 하나의 프로세스 때문에 다른 애들이 실행되지 못함을 의미한다.
따라서 비동기적 프로그래밍(Async Programming) 이 나오게 되었다.
비동기 작업을 사용하면 다른 작업이 완료되기를 기다리는 동안 프로그램이 작업을 완료할 수 있어서 다음상황에서 많이 사용된다.(외부에서 데이터를 다루는 동안 다른 작업을 하기 위해!)
다트 언어에는 async programming 을 위해 다음과 같은 것이 필요하다.
비동기 작업은 결과는 보통 Future로 제공하고, 결과가 다수의 파트를 가지고 있다면 Stream으로 제공한다.
Future란 결과가 미래에 언젠가 제공 될 것이라는 약속과 비슷하고, Stream은 이벤트와 같은 일련의 값을 가져 오는 방법이다.
이 비동기성을 다루기 위해서는 다른 일반적인 Dart 함수들도 비동기화되어야 한다.(async, await 사용)
비동기 결과와 상호작용하려면 async와 await 키워드를 사용한다.
보통 함수를 선언할 때, 함수 이름 뒤 본문이 시작하는 중괄호 { 앞에 async 키워드를 붙여 비동기로 만들 수 있다.
비동기 함수 안에서 언제 끝날지 모르는 작업 앞에는await 키워드를 붙인다.
이렇게 함수들을 작업을 마친 결과를 받기 위해 비동기 함수 이름 앞에 Future (값이 여러 개면 Stream) 클래스를 지정한다.
MVVM 구조의 날씨 어플 실습의 현재 폴더 구조는 다음과 같다.
다시 리뷰를 해보자면 View, ViewModel, Model 로 구분하여 작성하는 것이 MVVM 의 핵심이었다.
View 는 사용자에게 보이는 화면을 담고 있고, ViewModel 에는 View로부터 독립적이며, View가 필요로 하는 데이터만을 소유하고 있다. model 은 데이터와 데이터 조작을 담당하였다.
아래 폴더에서는 ViewModel, View, Utilities, Resources, Repository, Model, Data 폴더로 이루어져 있다. ViewModel 은 아직 작성하지 않았으며, View 에는 스플래시 스크린을 만들었다.(스플래시스크린)
그리고 Utilities 에는 문자 포맷을 위한 헬퍼함수들을 정의하였고, 리소스에는 이미지, 색상, URl등 리소스들을 처리하는 폴더이다. 그리고 Model 은 데이터를 조작하기 위한 코드를 작성하였다. Data 폴더는 외부 api 로 날씨 정보를 받아오므로 오류 발생시 에러 처리 코드와 외부와 소통하기 위한 Network 폴더에는 오늘 작성할 것이다.
)
우선 Network 폴더 내에 api_services.dart 와 base_api_services.dart 두 가지의 파일을 생성한다.
BaseApiServices는 API 서비스에 대한 기본 틀 또는 규약을 정의하는 추상 클래스이다. 이 클래스는 애플리케이션에서 사용되는 모든 API 서비스의 기본 인터페이스를 제공하는 클래스이다.
추상 클래스는 구체적인 구현을 포함하지 않고, 메서드의 시그니처(예: Future getApi(String url))만을 정의하고, 이러한 메서드들은 상속받는 클래스에서 구현한다.
왜 추상 클래스로 작성할까?
그냥 추상 클래스로 정의해도 어차피 상속받아서 구현해야하는 건 똑같은데 굳이 추상 클래스로 정의하는 이유가 무엇일까? 바로 재사용성과 확장성 측면에서 이점이 있기 때문이다.
재사용성 및 확장성: 다양한 API 서비스 클래스들이 이 기본 클래스를 상속받음으로써 일관된 방식으로 네트워크 요청을 처리할 수 있다. 모든 API 서비스 클래스들이 동일한 기본 인터페이스를 따르도록 강제하여 새로운 API 서비스를 추가할 때 기준이 되는 틀을 제공해줄 수 있다.이는 코드의 재사용성과 확장성을 높여줄 수 있다. 공통이 되는건 Base 에서 정의하고 세부적으로 다른 건 각자의 클래스에서 수정하면 된다.
이러한 접근 방식은 특히 크고 복잡한 애플리케이션에서 유용하며, 소프트웨어 설계 원칙인 DRY(Don't Repeat Yourself)와 OCP(Open-Closed Principle)을 준수하는 데 도움을 준다.
abstract class BaseApiServices
{
Future<dynamic> getApi(String url);
}
ApiServices 클래스는 BaseApiServices의 구체적인 구현을 제공한다. 이 클래스에서는 실제 네트워크 요청을 실행하고, 응답을 처리하는 로직을 포함하게 된다.
api_services.dart 파일에서는 BaseApiServices에서 정의된 메서드들을 오버라이드(재정의)하여 실제 HTTP 요청을 보내고, 응답을 처리하는 코드를 작성한다. 이 클래스는 특정한 API 서비스에 맞춰진 기능을 구현할 수 있으며, 다양한 API 엔드포인트에 대한 요청 처리 방식을 포함할 수 있다.
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:mvvm_weather_with_apis_getx/Data/Network/base_api_services.dart';
import 'package:mvvm_weather_with_apis_getx/Data/app_exceptions.dart';
class ApiServices extends BaseApiServices{
// dynamic : can have any type (function can return any type)
dynamic jsonResponse(http.Response response){
switch(response.statusCode){
case 200:
// successful http resposne
// 반응이 정상적이라면 응답의 바디 부분을 디코드하여 결과값으로 지정
var jsonResponse = jsonDecode(response.body);
return jsonResponse;
case 400:
// BAD_REQUEST
var jsonResponse = jsonDecode(response.body);
return jsonResponse;
default:
throw FetchDataException(
'Error while communication ${response.statusCode}'
);
}
}
Future getApi(String url) async{
// async : 동기화
var jsonData;
try {
var response = await http.get(Uri.parse(url)).timeout(const Duration(seconds:10));
jsonData = jsonResponse(response);
} on RequestTimeOut
{
throw RequestTimeOut('Request Timeout');
} on SocketException
{
throw InternetException('No internet');
}
return jsonData;
}
}
코드를 살펴보면
일단 해당 코드 전체는 http 패키지를 사용하게 된다. http 패키지는 pub.dev에서 검색해서 찾을 수 있다.
pub.dev
검색후 해당 패키지 Installing 을 누르면 dependency 에 추가할 게 나오는데 복붙해주면 된다.
나는 pubspec.yaml 파일에서 dependencies 내부에 http: ^1.1.2 를 붙여넣기 해줬다.
ㅁjssonResponse 메소드부터 살펴보자. 리턴값은 dynaic 으로 지정한다.
dynamic 은 해당 함수가 어떠한 타입도 리턴할 수 있음을 의미하는 키워드이다.
Response 객체를 인자로 입력받아서 response 의 상태코드에 따라 다른 처리를 해준다.
만약 200이면 OK 라는 뜻으로 바디값(jsonString)을 jsonDecode() 를 통해 jsonResponse 에 담아준다. 타입은 var 로 알아서 타입을 지정해준다. 그걸 리턴해준다. 400이면 bad request 로 같은 처리를 해주고 둘 다 아니면 Data 를 가져오는데 실패했다는 에러 처리를 해준다.
http 상태코드는 아래 표를 참고하면 된다.
다음 함수는 getApi 메소드이다. 해당 함수는 비동기 처리를 해준다. 반환 타입은 Future 로 비동기 작업의 결과를 담을 때 사용한다. 예외 처리를 위한 try 블록을 통해 작업을 수행한다. 이 블록 내에서 네트워크 요청을 시도하고, 예외(예: 네트워크 오류, 타임아웃 등)가 발생하면 catch 블록으로 이동하게 된다.
http.get(Uri.parse(url)): 주어진 URL 문자열을 Uri 객체로 파싱한 후, 해당 URL로 HTTP GET 요청을 보낸다.
await: 이 키워드는 비동기 호출이 완료될 때까지 대기한다. http.get 메서드가 반환하는 Future 객체의 결과가 준비될 때까지 실행을 일시 중단하게 된다.
.timeout(const Duration(seconds:10)): 요청이 10초 이내에 완료되지 않으면 자동으로 타임아웃이 발생하도록 설정한다.
결과는 response 변수에 저장하고, jsonData 에 response을 분석하고 처리한 후 결과 데이터를 저장한다.
try 블록 내에서 RequestTimeOut 예외가 발생하면 아래 블록이 처리되고, SocketException 예외가 발생하면 그 아래 블록이 실행된다.
그 후 해당 메소드는 jsonData 를 리턴한다.