728x90

Flutter에서 서버 데이터 가져오기를 Retrofit 라이브러리를 활용하는 법을 구현해보고 적어둔다.

JSON Serializable 에서 설명한 사항은 건너뛰고 나머지를 설명한다.

 

pubspec.yaml 파일에서 필요한 라이브러리

dependencies:
  dio: ^5.4.0  
  json_annotation: ^4.8.1
  retrofit: ^4.0.3
 
dev_dependencies:  
  json_serializable: ^6.7.1  
  build_runner: ^2.4.7
  retrofit_generator: ^8.0.6

 

Model 구현

import 'package:json_annotation/json_annotation.dart';
 
part 'contact_item.g.dart';
 
@JsonSerializable()
class ContactItem {
  final int idx;
  final String userNM;
  final String mobileNO;
  final String? telNO;
  final String? photo;
  final bool checkBoxState;
 
  const ContactItem({
    required this.idx,
    required this.userNM,
    required this.mobileNO,
    required this.telNO,
    required this.photo,
    required this.checkBoxState,
  });
 
  factory ContactItem.fromJson(Map<String, dynamic> json) =>
      _$ContactItemFromJson(json);
 
  Map<String, dynamic> toJson() => _$ContactItemToJson(this);
}
 

 

서버에서 제공하는 JSON 데이터를 추출하기 위한 모델 클래스

import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
 
part 'contact_result.g.dart';
 
@JsonSerializable()
class ContactResult {
  final String status;
  final String message;
  final List<ContactItem>? addrinfo;
 
  const ContactResult({
    required this.status,
    required this.message,
    this.addrinfo,
  });
 
  factory ContactResult.fromJson(Map<String, dynamic> json) =>
      _$ContactResultFromJson(json);
 
  Map<String, dynamic> toJson() => _$ContactResultToJson(this);
}

 

2개의 클래스 구현 후에 dart run build_runner build 를 해주면 자동으로 g.dart 파일이 생성된다.

 

Retrofit 라이브러리 구현

클래스명은 RestClient 라고 했지만 ContactRepository 라로 해도 될 듯하다.

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:retrofit_ex2/common/repository/retrofit_url.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
import 'package:retrofit_ex2/model/contact_result.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @GET(RetrofitURL.contactData)
  Future<ContactResult> getContactList();
 
  @GET(RetrofitURL.allcontactData)
  Future<List<ContactItem>> getAllContactData();
 
  @POST(RetrofitURL.postContactData)
  @FormUrlEncoded()
  Future<ContactResult> postContactList(
    @Field() String keyword,
    @Field() String search,
  );
}
 
/***
 * 서버에는 파라미터가 다르게 되어 있지만
 * 위에 3개의 함수는 모두 같이 List<ContactItem> 을 반환할 수 있다.
 * https://pub.dev/packages/retrofit/example 를 참조하면서 함수를 테스트하면 좋을 듯 하다.
 *
 * 함수를 추가하거나 수정하면 반드시 dart run build_runner build 를 해줘야 한다.
 */
 

 

 

JSON 메시지를 출력하는 서버 코드 이름을 모아서 RetofitURL 파일로 구현했다.

class RetrofitURL {
  static const baseUrl = "https://www.abc.com";
  static const mLogin = "/androidSample/loginChk.php";
  static const contactData = "/androidSample/flutter/getContactList.php";
  static const allcontactData = "/androidSample/flutter/getAllContactList.php";
  static const photoPath = "/androidSample/photos/";
 
  static const postContactData = "/androidSample/flutter/postContactList.php";
}

 

파일이 PHP인 것은 중요하지 않다.

서버에서 출력하는 JSON 으로부터 모델 클래스를 구현할 줄 아는 것이 중요하다.

PHP를 사용하는 이유는 회사 프로젝트 개발시 반드시 구현해야 하는 Secure Coding까지 할 줄 아는 유일한 서버 언어다. 다른 언어인 Python, Node.js 는 약간 알기는 하지만, 회사 프로젝트 개발에는 아직 적용할 수 없다.

APP 구현 필수사항

- 서버와 앱간에 로그인시 패스워드는 반드시 RSA 암호화해서 데이터를 전송해야 한다.

- 해커가 알아서는 안되는 중요 정보는 AES256 암호화/복호화 처리를 한다.

- 서버 데이터를 가져오기 위해 토큰이나 세션처리를 해야 한다.

- 앱 루팅탐지 기능을 포함해야 한다.

- 앱 최종 배포시에는 난독화 적용을 해야 한다.

- 중요 데이터는 SQLite 같은 Local DB에 저장하지 못하도록 모의해킹 검증자가 지적 하더라.

서버에서는 SQL Injection 방지, XSS필터 구현, 중복 로그인 방지, RSA 복호화 처리, 해킹 시도 탐지 및 방어코드, 모든 로그인 시도내역 기록 등 구현할 사항이 많다.

GET방식의 코드부터 시작해서 POST로 Parameter 전달하여 key가 맞지 않으면 데이터를 반환하지 못하도록 처리하는 것 등을 포함하기 위해서 샘플을 3가지 구현해봤다.

 

 

여기까지 기본 정보를 구현하고 나서 실제 출력코드는 아래 예시를 살펴보자.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:retrofit_ex2/common/component/contact_card.dart';
import 'package:retrofit_ex2/common/repository/crypto_api.dart';
import 'package:retrofit_ex2/model/contact_item.dart';
import 'package:retrofit_ex2/model/contact_result.dart';
import 'package:retrofit_ex2/repository/rest_client.dart';
 
class ContactPostResultPage extends StatefulWidget {
  const ContactPostResultPage({Key? key}) : super(key: key);
 
  @override
  State<ContactPostResultPage> createState() => _ContactListPageState();
}
 
class _ContactListPageState extends State<ContactPostResultPage> {
  late final RestClient restClient;
 
  @override
  void initState() {
    Dio dio = Dio();
    restClient = RestClient(dio);
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: FutureBuilder<ContactResult>(
          future: restClient.postContactList(
              Crypto.AES_encrypt(Crypto.URLkey()), ''),
          builder:
              (BuildContext context, AsyncSnapshot<ContactResult> snapshot) {
            if (!snapshot.hasData) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
 
            final ids = snapshot.data as ContactResult;
 
            if (ids.status.contains("success")) {
              final addrinfo = ids.addrinfo as List<ContactItem>;
              return _ListViewSubPage(posts: addrinfo);
            } else {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      ids.status,
                      style: TextStyle(
                        color: Colors.red,
                        fontSize: 22.0,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    Text(ids.message),
                  ],
                ),
              );
            }
          },
        ),
      ),
    );
  }
}
 
class _ListViewSubPage extends StatelessWidget {
  final List<ContactItem> posts;
 
  const _ListViewSubPage({
    super.key,
    required this.posts,
  });
 
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final item = posts[index];
        // 누르면 다른 페이지로 이동하도록 하려면 GestureDetector 위젯으로 감싸고,
        // onTap 에 이동할 사항을 구현한다.
        return ContactCard.fromJson(item);
      },
      separatorBuilder: (context, index) => SizedBox(height: 2.0),
    );
  }
}
 

RestClient 호출하는 부분과 POST 변수 2개를 넣어서 전달하는 코드가 포함되어 있다.

keyword 변수는 AES256 암호화하여 전달하고, search 값은 ''으로 넣어서 모든 데이터를 가져오도록 처리했다.

Retrofit 라이브러리를 이용하면 함수 세부내용을 구현하지 않고 간단하게 호출만 하면 결과를 반환해준다.

FutureBuilder 사용하면 서버 전송 결과를 snapshot.data 로 반환한다.

snapshot.data를 ContactResult 클래스로 캐스팅하고, 다시 List<ContactItem> 배열을 반환하도록 addrinfo 변수로 캐스팅했다.

ListView.seperated 로 반복 출력하도록 한다.

가독성을 높이기 위해 내부 클래스를 추가하여 데이터를 넘기고 처리하는 것을 확인할 수 있다.

 

 

위에 나오는 ContactCard.fromJson 은 Retofit 활용법과는 별개이므로 설명하지 않는다.

 

블로그 이미지

Link2Me

,