티스토리 뷰

Flutter는 네트워킹과 데이터 처리 작업을 쉽게 수행할 수 있는 다양한 도구와 기능을 제공합니다. 이번 포스트에서는 백그라운드에서 데이터 파싱을 처리하는 방법에 대해 알아보고, 이를 통해 애플리케이션 성능을 최적화하는 방법을 소개합니다.

참고. Parse JSON in the background

왜 백그라운드 데이터 파싱이 중요한가?

백그라운드 데이터 파싱은 대규모 데이터를 처리하거나 복잡한 변환 작업이 필요한 경우 애플리케이션의 메인 스레드의 부하를 줄이고 UI 성능을 향상시키기 위해 필수적입니다. 메인 스레드가 데이터 처리로 인해 블록되지 않으면 애플리케이션은 사용자에게 더 부드럽고 반응성 있는 경험을 제공합니다.

Flutter에서 백그라운드 데이터 파싱 설정하기

Flutter에서는 compute 함수를 사용하여 백그라운드 스레드에서 데이터를 처리할 수 있습니다. 이 함수는 별도의 Isolate(독립된 메모리 공간)를 생성하여 데이터 파싱과 같은 무거운 작업을 처리합니다.

기본 코드 구조

import 'dart:convert';
import 'package:flutter/foundation.dart';

// JSON 데이터를 파싱하는 함수
List<dynamic> parseJson(String jsonData) {
  final parsed = jsonDecode(jsonData);
  return parsed;
}

// 백그라운드에서 데이터를 파싱
Future<List<dynamic>> fetchAndParseData(String jsonData) async {
  return compute(parseJson, jsonData);
}

단계별 구현

1. JSON 데이터 준비

Flutter 애플리케이션에서 JSON 데이터를 서버에서 가져오거나 로컬 파일에서 로드합니다.

import 'package:http/http.dart' as http;

Future<String> fetchJsonData() async {
  final response = await http.get(Uri.parse('https://example.com/data.json'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('데이터 로드 실패');
  }
}

2. 백그라운드 데이터 파싱 호출

fetchJsonData 함수로 가져온 데이터를 compute를 통해 백그라운드에서 처리합니다.

void processData() async {
  final jsonData = await fetchJsonData();
  final parsedData = await fetchAndParseData(jsonData);
  print(parsedData);
}

실전 예제: Flutter에서 백그라운드 데이터 파싱 사용하기

다음은 Flutter 애플리케이션에서 백그라운드 데이터 파싱을 활용한 실제 예제입니다.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('백그라운드 데이터 파싱')),
        body: DataList(),
      ),
    );
  }
}

class DataList extends StatefulWidget {
  @override
  _DataListState createState() => _DataListState();
}

class _DataListState extends State<DataList> {
  late Future<List<dynamic>> _data;

  @override
  void initState() {
    super.initState();
    _data = fetchAndParseData();
  }

  Future<List<dynamic>> fetchAndParseData() async {
    final response =
        await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
    if (response.statusCode == 200) {
      return compute(parseJson, response.body);
    } else {
      throw Exception('데이터 로드 실패');
    }
  }

  static List<dynamic> parseJson(String jsonData) {
    return jsonDecode(jsonData);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<dynamic>>(
      future: _data,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError) {
          return Center(child: Text('에러 발생: ${snapshot.error}'));
        } else {
          return ListView.builder(
            itemCount: snapshot.data?.length ?? 0,
            itemBuilder: (context, index) {
              final item = snapshot.data![index];
              return ListTile(
                title: Text(item['title']),
                subtitle: Text(item['body']),
              );
            },
          );
        }
      },
    );
  }
}

Flutter에서 compute 사용 시 주의점

  1. Isolate의 메모리 독립성: compute 함수는 기존 메모리 공간을 공유하지 않습니다. 따라서 전달하려는 데이터는 직렬화 가능한 상태여야 합니다.
  2. 데이터 크기 고려: 지나치게 큰 데이터를 전달하면 메모리 복사가 발생해 성능이 저하될 수 있습니다.
  3. UI 차단 방지: compute를 사용하여 UI 차단 없이 복잡한 작업을 처리할 수 있습니다.

백그라운드 데이터 파싱의 활용 사례

  1. 대용량 데이터 처리:
    • 뉴스 앱에서 수천 개의 기사를 로드하고 렌더링.
    • 전자상거래 앱에서 제품 데이터를 동적으로 불러오기.
  2. 복잡한 데이터 변환:
    • 서버에서 가져온 JSON 데이터를 Flutter 모델 클래스로 변환.
  3. 실시간 데이터 업데이트:
    • 실시간 주식 정보 앱 또는 스포츠 점수 업데이트.

결론

Flutter에서 백그라운드 데이터 파싱은 효율적인 데이터 처리를 통해 애플리케이션 성능을 향상시키는 중요한 기술입니다. compute 함수를 활용하면 간단하면서도 효과적으로 데이터 파싱을 처리할 수 있으며, 이를 통해 더욱 쾌적한 사용자 경험을 제공할 수 있습니다. 위의 예제를 바탕으로 여러분의 프로젝트에 백그라운드 데이터 파싱을 도입해 보세요.

 

import 'dart:async';
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client
      .get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));

  // Use the compute function to run parsePhotos in a separate isolate.
  return compute(parsePhotos, response.body);
}

// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed =
      (jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();

  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  const Photo({
    required this.albumId,
    required this.id,
    required this.title,
    required this.url,
    required this.thumbnailUrl,
  });

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      albumId: json['albumId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      url: json['url'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    const appTitle = 'Isolate Demo';

    return const MaterialApp(
      title: appTitle,
      home: MyHomePage(title: appTitle),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<List<Photo>> futurePhotos;

  @override
  void initState() {
    super.initState();
    futurePhotos = fetchPhotos(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder<List<Photo>>(
        future: futurePhotos,
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return const Center(
              child: Text('An error has occurred!'),
            );
          } else if (snapshot.hasData) {
            return PhotosList(photos: snapshot.data!);
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }
}

class PhotosList extends StatelessWidget {
  const PhotosList({super.key, required this.photos});

  final List<Photo> photos;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      itemCount: photos.length,
      itemBuilder: (context, index) {
        return Image.network(photos[index].thumbnailUrl);
      },
    );
  }
}