[MVVM Flutter-app 제작] #06 비동기 프로그래밍으로 json 데이터 불러오기 (Network layer) / Future, Async/Await

도톨이·2024년 1월 9일
0

앱 개발-flutter

목록 보기
8/29

해당 게시글은 Udemy 'Learn Flutter MVVM Architecture | Build Weather App' 강의와 dart 공식 문서를 참고하였습니다.
https://dart-ko.dev/codelabs/async-await

다트 언어에서는 프로그램 하나당 스레드 하나가 사용된다.
그 말은 만약 작업이 오래 걸리는 프로세스를 실행하면 그 하나의 프로세스 때문에 다른 애들이 실행되지 못함을 의미한다.

따라서 비동기적 프로그래밍(Async Programming) 이 나오게 되었다.

비동기 작업을 사용하면 다른 작업이 완료되기를 기다리는 동안 프로그램이 작업을 완료할 수 있어서 다음상황에서 많이 사용된다.(외부에서 데이터를 다루는 동안 다른 작업을 하기 위해!)

  • 네트워크를 통해 데이터를 가져올 때.
  • 데이터 베이스에 쓰기를 수행할 때.
  • 파일로부터 데이터를 읽을 때.

Async Programming

다트 언어에는 async programming 을 위해 다음과 같은 것이 필요하다.

Future API 사용

비동기 작업은 결과는 보통 Future로 제공하고, 결과가 다수의 파트를 가지고 있다면 Stream으로 제공한다.
Future란 결과가 미래에 언젠가 제공 될 것이라는 약속과 비슷하고, Stream은 이벤트와 같은 일련의 값을 가져 오는 방법이다.

이 비동기성을 다루기 위해서는 다른 일반적인 Dart 함수들도 비동기화되어야 한다.(async, await 사용)

Async await 사용

비동기 결과와 상호작용하려면 async와 await 키워드를 사용한다.

보통 함수를 선언할 때, 함수 이름 뒤 본문이 시작하는 중괄호 { 앞에 async 키워드를 붙여 비동기로 만들 수 있다.

비동기 함수 안에서 언제 끝날지 모르는 작업 앞에는await 키워드를 붙인다.

이렇게 함수들을 작업을 마친 결과를 받기 위해 비동기 함수 이름 앞에 Future (값이 여러 개면 Stream) 클래스를 지정한다.

  • 함수 이름 앞 Future은 반환 나타냄. 생략 가능
  • await를 사용하기 위해서는 반드시 async가 필요
  • await를 사용하면 비동기 함수가 끝날때까지 기다리며, await를 사용하지않으면 기다리지않는다.
  • 비동기함수가 끝났음을 알리고싶다면 Callback함수 이용

Practice

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 두 가지의 파일을 생성한다.

1. 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);

}

2. api_services.dart

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 를 리턴한다.

profile
Computer Engineering

0개의 댓글

관련 채용 정보