본문 바로가기

Flutter

[Flutter] isolate(compute)를 활용한 백그라운드에서 작업하기

개요

웹이나 로컬에서 대량의 데이터를 가져와 파싱하는 작업을 메인 쓰레드에서 진행할 시  UI가 버벅이거나 움직이지 않는 현상이 앱에서 종종 발생한다. 기존 안드로이드에서는 코루틴이나 RxJava를 통해 워커 쓰레드에서 이러한 작업을 처리했다. Flutter에서는 dart의 isolate을 활용한 compute 메서드를 통해 이 백그라운드에서 작업을 수행하여 화면의 버벅임이나 중단을 줄일 수 있다.

 

먼저 아래는 Flutter 공식 문서에서 compute를 활용하는 예시이다. 

응답의 body를 파싱하여 Photo 리스트로 변환하는 작업을 진행한다.

// 응답 결과를 List<Photo>로 변환하는 함수.
List<Photo> parsePhotos(String responseBody) {
  final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();

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

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

  return parsePhotos(response.body);
}

 

위 과정에서 데이터가 매우 많을 경우 버벅임이 발생할 수 있다. 따라서, isolate을 통해 백그라운드 작업을 진행하는 방법을 제시한다.

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

  // compute 함수를 사용하여 parsePhotos를 별도 isolate에서 수행합니다.
  return compute(parsePhotos, response.body);
}

 

 

실제 구현 중인 앱에서 활용하기

현재 주식 관련 애플리케이션을 제작 중이다. 로컬에는 국내, 미국의 상장된 주식 정보를 엑셀로 갖고 있다. 이를 백그라운드 작업 없이 수행할 경우 아래와 같이 동작한다.

첫 화면이 로딩되고 거의 10초간 아무일도 일어나지 않는 것 처럼 보인다. 위는 2배속을 해서 금방 UI들이 로딩이 되는 것 처럼 보이는 것이다.

 

위를 구현한 코드는 아래와 같다.

getData() async {
    ByteData data = await rootBundle.load(tickersPath);
    var bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    var excel = Excel.decodeBytes(bytes);
    for (var table in excel.tables.keys) 
    	for (var row in excel.tables[table].rows) 
      		list.add(SavedStock.fromXlsx(row));
    _subjectSavedStockList.sink.add(value);
  }

엑셀에서 데이터를 읽어오고 for문을 돌며 데이터를 파싱한다. (엑셀에는 대략 9700개의 row가 존재한다.)

 

 

위를 개선하기 위해 isolate을 활용하여 백그라운드에서 동작하게 하는 소스 코드는 다음과 같다.

getData() async {
    ByteData data = await rootBundle.load(tickersPath);
    var bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    var excel = Excel.decodeBytes(bytes);
    //isolate을 통해 백그라운드 스레드에서 연산
    compute(parseStock, excel).then((value) {
      _subjectSavedStockList.sink.add(value);
    });
  }

List<SavedStock> parseStock(Excel excel) {
  List<SavedStock> list = [];
  for (var table in excel.tables.keys) {
    for (var row in excel.tables[table].rows) 
      list.add(SavedStock.fromXlsx(row));
  }
  return list;
}

for문을 돌며 파싱하는 부분만 다시 메서드로 만들었다. compute를 통해 연산결과를 then을 통해 받을 수 있다. 참고로 한가지 중요한 점은 compute의 첫번째 인자로 메서드 두번째 인자로는 메서드에 필요한 인자를 전달한다. 이 때, 첫 번째 인자인 메서드는 클래스 외부 최상위에 구현되어 있어야 한다. 클래스 내부의 메서드이면 실행되지 않는다. 

 

위를 실제 실행한 화면은 아래와 같다.

앱을 구동하자마자 바로 UI가 동작하는 모습이다. 위는 심지어 배속도 없는 결과이다. (참고로 MyTickers 밑의 회색 UI는 네트워킹을 통해 추가로 데이터를 가져오는 중이라 로딩이 조금 더 걸리는 상태이다.)

 

 

후기

Flutter에서 백그라운드 작업을 코딩하는게 안드로이드 보다는 쉽다고 느꼈다. 추가로 신기한 점은 전자에서 메인 쓰레드만 이용해서 데이터를 가져오는데 로그를 찍어보니 거의 10초가 걸렸고, 백그라운드 쓰레드를 활용한 후자는 1초만에 가져왔다. 예상에는 둘다 데이터를 가져오는 시간 차이는 별로 없을 것이라 생각했다. 어차피 데이터틀 가져오는 작업은 전자, 후자 모두 각자의 쓰레드에서 가져오고 있으니까. 무언가 내가 모르는 부분이 존재하는 것 같다. 가능한 추측으로는 메인 쓰레드가 다시 여러 개의 작업으로 나누어지고 애니메이션 작업과 데이터 가져오는 작업을 동시에 해서 느리다..? 흠 어렵다. 굉장히 궁금한 부분인데 지인 중에 관련 전문가가 없으니 알 수가 없다...