[Flutter] 스나이퍼팩토리 24일차

KWANWOO·2023년 2월 27일
1
post-thumbnail

스나이퍼팩토리 플러터 24일차

24일차에는 데이터를 직렬화하는 Serialization에 대해 학습했다. 이번주는 클래스를 활용해 데이터를 처리하는 방식에 대해 학습할 예정이다.

학습한 내용

  • 데이터 직렬화 (Serialization)
  • Public API, Private API
  • factory 생성자

추가 내용 정리

데이터 직렬화 (Serialization)

데이터 직렬화(Serialization)는 메모리를 디스크에 저장하거나 네트워크 통신에 사용하기 위한 형식으로 변환하는 것을 의미한다.

데이터 직렬화 언어로는 yaml, xml, json 등이 있다.

역직렬화(Deserialization)는 반대로 디스크에 저장한 데이터를 읽거나, 네트워크 통신으로 받은 데이터를 메모리에 쓸 수 있도록 다시 변환하는 것이다.

API

API는 Application Programming Interface의 약자로 여러 프로그램들과 데이터베이스, 그리고 기능들의 상호 통신 방법을 규정하고 도와주는 매개체이다.

API는 아래와 같은 3가지 역할을 수행한다.

API의 역할
1. API는 서버와 데이터베이스에 대한 출입구 역할을 한다.
2. API느 애플리케이션과 기기가 원활하게 통신할 수 있도록 한다.
3. API는 기기와 운영체제에 상관없이 누구나 동일한 액세스를 얻을 수 있도록 모든 접속을 표준화한다.

이러한 API는 접근 방식에 따라 3가지의 유형으로 구분될 수 있다.

API의 유형
1. Private API
Private API는 내부 API로, 기업이나 연구 단체 등에서 자체 제품과 운영 개선을 위해 단체 내부에서만 사용한다. 따라서 제 3자에게 노출되지 않는다.

  1. Public API
    Public API는 개방형 API로, 모두에게 공개된다. Public API 중에서도 접속하는 대상에 대한 제약이 없는 경우에는 Open API라고 한다.

  2. Partner API
    Partner API는 특정 비즈니스 파트너 간의 데이터 공유를 위해 사용된다. 따라서 동의하는 특정인들만 사용할 수 있다.

또한 아키텍처 스타일에 따라 API의 종류를 구분할 수도 있는데 SOAP, RPC, REST API, GraphQL 등으로 구분된다.
(해당 포스팅에서 이 내용은 깊게 다루지 않음)

API는 아래와 같은 장점들이 있다.

API 장점
1. 개발자들이 애플리케이션 코드를 작성하는 방법을 표준화함으로써, 간소화되고 빠른 프로세스 처리를 가능하게 한다.
2. 소프트웨어를 통합할 때 개발자들 간의 협업을 용이하게 한다.

Factory

[Flutter] 스나이퍼팩토리 22일차에서 팩토리 생성자에 대해 간단히 정리했었다.

해당 포스팅에서 좀 더 자세한 내용을 작성하고자 한다.

Dart에서 factory는 새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 사용한다고 설명되어 있다. 이는 싱글톤 패턴을 구현하는 것과 같다.

싱글톤 패턴(singleton-pattern)
소프트웨어 디지인 패턴에서 싱글톤 패턴을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이러한 디자인 유형을 싱글톤 패턴이라고 한다.

factory는 아래와 같은 특징을 가진다.

factory 특징

  • 기존에 이미 생성된 인스턴스가 있다면 return하여 재사용한다.
  • 하나의 클래스에서 하나의 인스턴스만 사용한다.
  • 서브 클래스 인스턴스를 리턴할 때 사용할 수 있다.
  • 팩토리 생성자에서는 this에 접근할 수 없다.

아래 예시 코드에서 Logger()가 호출될 때마다 Logger factory생성자가 호출된다. 처음 호출된다면 인스턴스를 새로 생성하여 반환하고, 한 번 더 호출하면 기존 인스턴스를 반환한다.

class Logger {
  final String name;
  bool mute = false;

  static final Map<String, Logger> _cache =
      <String, Logger>{};

  // 1번
  factory Logger(String name) {
    return _cache.putIfAbsent( // 2번
        name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

또한 아래의 예시처럼 서브 클래스의 인스턴스를 반환할 때에도 사용할 수 있다.

class Book {

  // named generative
  Book.comic(String title) : this(title, "Champ"); 

  // named factory 
  factory Book.comic(String title) {
    return ComicBook(title);
  }
}

class ComicBook extends Book {
  ComicBook(String title) : super(title, "Champ");
}

24일차 과제

  1. 강아지 사진 앱 만들기
  2. 추가 내용 정리

1. 강아지 사진 앱 만들기

공개된 API를 분석하고, 클래스를 활용하여 강아지 사진을 보여주는 앱을 만들고자 한다.

요구사항

  • 아래의 API를 사용해 데이터를 요청한다.
  • 반드시 네트워크의 데이터를 받아와서 제작한 클래스에 적용해야 한다.
  • 3개의 강아지 사진이 갤러리 형태로 나오도록 만든다.
  • 강아지 사진을 클릭하면 강아지 사진이 Dialog로 나오게 한다.

결과물 예시

코드 작성

  • lib/main.dart
import 'package:flutter/material.dart';
import 'package:my_app/page/main_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // root Widget
  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: MainPage(), // 홈 페이지 호출
    );
  }
}

main.dart에서는 MainPage를 호출한다.

  • lib/model/Dog.dart
class Dog {
  String message;
  String status;

  Dog({
    required this.message,
    required this.status,
  });

  factory Dog.fromJson(Map<String, dynamic> json) {
    return Dog(
      message: json['message'],
      status: json['status'],
    );
  }
}

강아지의 사진 링크와 상태를 가지고 있는 모델 클래스를 Dog로 생성했다. 기본 생성자를 작성하고 팩토리 생성자를 json 값을 받아 멤버 변수에 매핑하여 직렬화하도록 작성했다.

  • lib/page/main_page.dart
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:my_app/model/Dog.dart';

class MainPage extends StatelessWidget {
  MainPage({super.key});

  final Dio dio = Dio(); //dio 객체
  final String url = 'https://dog.ceo/api/breeds/image/random'; //데이터 요청 url

  //데이터 가져오기
  Future<Dog> getData() async {
    var response = await dio.get(url); //데이터 요청

    //데이터를 정상적으로 받아온 경우
    if (response.statusCode == 200) {
      //데이터를 직렬화하여 리턴
      return Dog.fromJson(response.data);
    } else {
      throw Exception('Could not get data');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, //그리드뷰 한 줄에 3개의 요소
        ),
        itemCount: 3, //총 아이템 개수
        itemBuilder: (context, index) {
          return FutureBuilder(
            future: getData(), //데이터 요청 바인딩
            builder: (context, snapshot) {
              //데이터 로딩 중
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              }

              //데이터가 없는 경우
              if (!snapshot.hasData) return const Text('데이터 없음!');

              //데이터를 받아온 경우
              return GestureDetector(
                //사진 다이얼로그
                onTap: () => showDialog(
                  context: context,
                  builder: (context) {
                    return AlertDialog(
                      backgroundColor: Colors.white,
                      contentPadding: const EdgeInsets.all(0),
                      actionsAlignment: MainAxisAlignment.center,
                      clipBehavior: Clip.antiAlias,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(16), //테두리 곡선
                      ),
                      //이미지 출력
                      content: Image.network(
                        fit: BoxFit.contain,
                        snapshot.data!.message,
                      ),
                      actions: [
                        //닫기 버튼
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text('Close'),
                        )
                      ],
                    );
                  },
                ),
                //그리드뷰 아이템 이미지
                child: Image.network(
                  fit: BoxFit.cover,
                  snapshot.data!.message,
                  loadingBuilder: ((context, child, loadingProgress) {
                    //이미지 로딩 완료
                    if (loadingProgress == null) {
                      return child;
                    }
                    //이미지 로딩 중
                    return const Center(child: LinearProgressIndicator());
                  }),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

getData()는 네트워크에 데이터를 요청하여 가져와 Dog 클래스에 직렬화를 수행하여 리턴하는 함수이다.

본문에서는 그리드 뷰를 사용해 갤러리를 구성한다. 한 줄에 3개의 아이템이 출력되도록 설정했고, 총 3개의 아이템을 가져오도록 itemCount를 3으로 설정했다.

그리드 뷰의 요소로는 FutureBuilder를 사용해 앞에서 작성한 getData()로 강아지 사진에 대한 데이터를 가져왔다.

데이터를 성공적으로 받아온 경우 GestureDetectoronTap이벤트로 다이얼로그를 띄워주었다. 다이얼로그에서 이미지를 출력했으며 텍스트버튼을 눌렀을 때, 다이얼로그를 닫는 기능을 추가했다.

마지막으로 본문에서는 Image.network를 사용해 이미지를 출력했는데 여기서 loadingBuilder를 사용해 이미지 로딩중에는 LinearProgressIndicator를 보여주도록 했다.

결과

코드에서 GridView.builderitemCount의 값을 변경하면 더 많은 사진을 불러올 수 있다. 아래는 itemCount를 30으로 변경한 결과이다. 더욱 갤러리 같은 화면을 만들 수 있다.

2. 추가 내용 정리

과제로 강아지 사진 앱을 제작하고 강의를 수강한 뒤 아쉬웠던 점을 정리했다.

getData() nullable

getData()에서 데이터를 받아왔을 때 올바르지 않은 null 값이 전달 될 수도 있다. 따라서 getData() 아래와 같이 nullable로 작성하면 더 좋을 것 같다.

  Future<Dog?> getData() async {
    var response = await dio.get(url); //데이터 요청

    //데이터를 정상적으로 받아온 경우
    if (response.statusCode == 200) {
      //데이터를 직렬화하여 리턴
      return Dog.fromJson(response.data);
    }
    return null
  }

Image.network null 체크

Image.network에 들어가는 image String이 null인 경우를 확인하여 아래와 같이 값을 다르게 리턴하면 더 안전하고 좋은 코드가 될 수 있다.

return snapshot.data?.message == null 
		? SizedBox()
        : GestureDetector(
			...
		);

요구사항 확인

"3개의 강아지 사진이 갤러리 형태로 나오도록 만드시오."라는 요구사항을 보고 3개의 요소를 넣었는데 한 줄에 3개의 요소를 출력한다는 의미였던 것 같다. ㅠㅠ

그리드뷰에서 아이템이 무한으로 생성되도록 하려면 itemCount를 설정하지 않으면 된다.

추가로 showDialog()에서 리턴한 AlertDialog는 따로 widget 폴더에 dog_detail_dialog.dart와 같은 파일로 생성해서 만들었으면 코드가 간결해지고 좋았을 것 같다.


6주차 시작

스나이퍼팩토리 6주차가 시작되었다. 이번주에는 클래스를 활용하여 데이터를 직렬화하는 것을 반복적으로 할 예정이라고 한다. 아주 중요한 내용이니까 열심히 따라가야겠다!! 어제 도전과제가 어려워서 그걸 하고 오늘 과제를 하니 좀 쉽게 느껴진다. ㅋㅋㅋㅋ 6주차 첫 번째 날은 여기서 마무리

📄Reference

profile
관우로그

0개의 댓글