Flutter에서 Isolate를 어떻게 사용하나요?

질문

Flutter에서 Isolate를 사용하여 백그라운드 작업을 처리하는 방법과 UI 스레드와 통신하는 방법을 알려주세요.

답변

Flutter에서 Isolate는 무거운 계산 작업을 메인 UI 스레드에서 분리하여 앱의 성능을 향상시키는 중요한 기능입니다. 멀티스레딩과 유사하지만, Dart의 Isolate는 메모리를 공유하지 않고 메시지 패싱(message passing)을 통해서만 통신합니다.

1. Isolate의 기본 개념

Isolate는 다음과 같은 특징을 가집니다:

  • 각 Isolate는 자체 메모리 힙을 가짐
  • Isolate들은 메모리를 공유하지 않음
  • Isolate 간 통신은 메시지 패싱으로만 가능
  • Isolate는 싱글 스레드로 동작

Flutter 앱은 기본적으로 하나의 Isolate(메인 또는 UI Isolate)에서 실행됩니다. 이 메인 Isolate는 UI 렌더링, 사용자 입력 처리, 애니메이션 등을 담당합니다. 따라서 무거운 계산 작업을 메인 Isolate에서 실행하면 UI가 버벅거리게 됩니다.

2. 기본 Isolate 사용법

가장 기본적인 Isolate 사용 방법을 살펴보겠습니다:

import 'dart:isolate';

void main() async {
  // Isolate 생성을 위한 ReceivePort
  final receivePort = ReceivePort();

  // Isolate 생성
  await Isolate.spawn(
    heavyComputation,   // Isolate에서 실행할 함수
    receivePort.sendPort, // 통신을 위한 SendPort
  );

  // Isolate로부터 결과 수신
  final result = await receivePort.first;
  print('계산 결과: $result');
}

// Isolate에서 실행될 함수
void heavyComputation(SendPort sendPort) {
  // 무거운 계산 작업 수행
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }

  // 결과를 메인 Isolate로 전송
  sendPort.send(sum);
}

위 코드에서:

  1. ReceivePort를 생성하여 메시지를 수신할 채널을 엽니다.
  2. Isolate.spawn을 사용하여 새 Isolate를 생성하고, 실행할 함수와 통신을 위한 SendPort를 전달합니다.
  3. 새 Isolate에서 무거운 계산을 수행하고 결과를 메인 Isolate로 전송합니다.
  4. 메인 Isolate는 receivePort.first를 통해 결과를 기다립니다.

3. 양방향 통신 구현하기

Isolate와의 양방향 통신을 구현하려면 양쪽 모두 ReceivePortSendPort가 필요합니다:

import 'dart:isolate';

void main() async {
  // 메인 Isolate의 ReceivePort
  final mainReceivePort = ReceivePort();

  // 새 Isolate 생성
  final isolate = await Isolate.spawn(
    workerIsolate,
    mainReceivePort.sendPort,
  );

  // 워커 Isolate로부터 초기 메시지(SendPort) 수신
  SendPort workerSendPort = await mainReceivePort.first;

  // 양방향 통신을 위한 새 ReceivePort
  final communicationReceivePort = ReceivePort();

  // 워커에게 통신 채널 정보 전송
  workerSendPort.send([communicationReceivePort.sendPort, 42]);

  // 워커로부터 결과 수신
  communicationReceivePort.listen((message) {
    print('워커로부터 받은 결과: $message');

    // 작업이 완료되면 Isolate와 ReceivePort 정리
    if (message == 'DONE') {
      communicationReceivePort.close();
      mainReceivePort.close();
      isolate.kill();
    }
  });
}

// 워커 Isolate 함수
void workerIsolate(SendPort mainSendPort) {
  // 워커의 ReceivePort 생성
  final workerReceivePort = ReceivePort();

  // 메인 Isolate에게 통신을 위한 SendPort 전송
  mainSendPort.send(workerReceivePort.sendPort);

  // 메인 Isolate로부터 메시지 수신
  workerReceivePort.listen((message) {
    if (message is List) {
      final SendPort communicationSendPort = message[0];
      final int data = message[1];

      // 데이터 처리
      final result = processData(data);

      // 결과 전송
      communicationSendPort.send(result);

      // 작업 완료 신호 전송
      communicationSendPort.send('DONE');
    }
  });
}

int processData(int data) {
  // 무거운 계산 수행
  int result = 0;
  for (int i = 0; i < data * 1000000; i++) {
    result += i;
  }
  return result;
}

4. compute 함수 사용하기

Flutter는 compute 함수를 제공하여 간단하게 Isolate를 생성하고 처리할 수 있게 합니다:

import 'package:flutter/foundation.dart';

void main() async {
  // compute를 사용하여 무거운 작업 실행
  final result = await compute(heavyComputation, 1000000);
  print('계산 결과: $result');
}

// 무거운 계산 함수
int heavyComputation(int iterations) {
  int sum = 0;
  for (int i = 0; i < iterations; i++) {
    sum += i;
  }
  return sum;
}

compute 함수의 장점:

  • 간결한 코드로 Isolate 생성 및 통신 처리
  • 자동으로 Isolate 생명주기 관리
  • 오류 처리 내장

단점:

  • 함수와 매개변수는 반드시 최상위 함수 또는 정적 메서드여야 함
  • 복잡한 양방향 통신이 필요한 경우에는 적합하지 않음

5. 실제 앱에서의 Isolate 사용 예시

5.1 이미지 처리 예시

import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;

class ImageProcessingScreen extends StatefulWidget {
  @override
  _ImageProcessingScreenState createState() => _ImageProcessingScreenState();
}

class _ImageProcessingScreenState extends State<ImageProcessingScreen> {
  Uint8List? originalImage;
  Uint8List? processedImage;
  bool isProcessing = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Isolate 이미지 처리')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (originalImage != null) ...[
              Image.memory(
                originalImage!,
                width: 200,
                height: 200,
              ),
              SizedBox(height: 20),
              if (isProcessing)
                CircularProgressIndicator()
              else if (processedImage != null)
                Image.memory(
                  processedImage!,
                  width: 200,
                  height: 200,
                ),
            ],
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _loadAndProcessImage,
              child: Text('이미지 로드 및 처리'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _loadAndProcessImage() async {
    // 원본 이미지 로드 (실제 앱에서는 asset 또는 파일에서 로드)
    final ByteData data = await rootBundle.load('assets/sample_image.jpg');
    final Uint8List bytes = data.buffer.asUint8List();

    setState(() {
      originalImage = bytes;
      isProcessing = true;
    });

    // Isolate를 사용하여 이미지 처리
    final processedBytes = await compute(applyGreyscaleFilter, bytes);

    setState(() {
      processedImage = processedBytes;
      isProcessing = false;
    });
  }
}

// Isolate에서 실행될 이미지 처리 함수
Uint8List applyGreyscaleFilter(Uint8List bytes) {
  // 이미지 디코딩
  final image = img.decodeImage(bytes);
  if (image == null) return bytes;

  // 그레이스케일 필터 적용
  final greyscale = img.grayscale(image);

  // 이미지 인코딩하여 바이트 배열로 반환
  return Uint8List.fromList(img.encodePng(greyscale));
}

5.2 데이터 파싱 예시

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

class JsonParsingScreen extends StatefulWidget {
  @override
  _JsonParsingScreenState createState() => _JsonParsingScreenState();
}

class _JsonParsingScreenState extends State<JsonParsingScreen> {
  List<Post> posts = [];
  bool isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Isolate JSON 파싱')),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: posts.length,
              itemBuilder: (context, index) {
                final post = posts[index];
                return ListTile(
                  title: Text(post.title),
                  subtitle: Text(post.body),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _fetchPosts,
        child: Icon(Icons.refresh),
      ),
    );
  }

  Future<void> _fetchPosts() async {
    setState(() => isLoading = true);

    try {
      // API 호출
      final response = await http.get(
          Uri.parse('https://jsonplaceholder.typicode.com/posts'));

      if (response.statusCode == 200) {
        // Isolate를 사용하여 JSON 파싱
        final parsedPosts = await compute(parsePosts, response.body);

        setState(() {
          posts = parsedPosts;
          isLoading = false;
        });
      } else {
        throw Exception('Failed to load posts');
      }
    } catch (e) {
      setState(() => isLoading = false);
      print('Error: $e');
    }
  }
}

// Isolate에서 실행될 JSON 파싱 함수
List<Post> parsePosts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
  return parsed.map<Post>((json) => Post.fromJson(json)).toList();
}

class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

6. Isolate를 사용할 때 고려해야 할 사항

6.1 직렬화 가능한 데이터만 전송 가능

Isolate 간에 전송되는 데이터는 직렬화가 가능해야 합니다:

  • 기본 타입(int, double, bool, String)
  • Lists, Maps, Sets
  • 직렬화 가능한 객체들로 구성된 컬렉션

Isolate 간에 전송할 수 없는 것들:

  • 함수 객체
  • 클로저
  • 스트림
  • 파일 핸들
  • 소켓
  • 네이티브 리소스

6.2 Isolate 생성 비용

Isolate 생성은 비용이 큰 작업입니다:

  • 메모리 사용 증가
  • 초기화 시간 소요
  • 컨텍스트 스위칭 비용

따라서 아주 간단한 작업에는 Isolate를 사용하지 않는 것이 좋습니다.

6.3 메시지 전송 비용

메시지를 전송할 때 직렬화/역직렬화가 발생하므로, 큰 데이터를 자주 주고받는 것은 피해야 합니다.

7. Flutter web에서의 Isolate

Flutter web에서는 Isolate 지원이 제한적입니다. 웹 환경에서는 다음과 같은 대안을 고려할 수 있습니다:

  • compute 함수 사용 (내부적으로 웹 워커를 사용)
  • 작은 작업으로 분할하여 Future.microtask 또는 Future.delayed 사용
  • 웹 워커 API 직접 사용

8. Isolate 관련 패키지

8.1 isolate_handler

더 쉬운 Isolate 관리를 위한 패키지입니다:

import 'package:isolate_handler/isolate_handler.dart';

void main() async {
  // 핸들러 생성
  final isolateHandler = IsolateHandler();

  // 메시지 리스너 설정
  isolateHandler.addListener('calculation', (dynamic data) {
    print('결과: $data');
  });

  // Isolate 생성 및 작업 시작
  isolateHandler.spawn(
    calculationIsolate,
    name: 'calculation',
    onInitialized: () => isolateHandler.send('start', 1000000, to: 'calculation'),
  );
}

// Isolate 함수
void calculationIsolate(Map<String, dynamic> context) {
  // 핸들러 초기화
  final messenger = HandledIsolate.initialize(context);

  // 메시지 리스너
  messenger.listen((data) {
    if (data == 'start') {
      final iterations = messenger.getArgument(0);
      int sum = 0;
      for (int i = 0; i < iterations; i++) {
        sum += i;
      }
      messenger.send(sum);
    }
  });
}

8.2 flutter_isolate

Flutter에서 백그라운드 Isolate를 쉽게 관리할 수 있는 패키지:

import 'package:flutter_isolate/flutter_isolate.dart';
import 'dart:isolate';

void main() async {
  // 통신을 위한 ReceivePort
  final receivePort = ReceivePort();

  // FlutterIsolate 생성
  final isolate = await FlutterIsolate.spawn(
    isolateFunction,
    receivePort.sendPort,
  );

  // 메시지 수신 및 처리
  receivePort.listen((message) {
    print('Received: $message');

    if (message == 'DONE') {
      receivePort.close();
      isolate.kill();
    }
  });
}

void isolateFunction(SendPort sendPort) {
  // 작업 수행
  int result = performHeavyTask();

  // 결과 전송
  sendPort.send(result);
  sendPort.send('DONE');
}

int performHeavyTask() {
  // 무거운 계산
  return 42;
}

결론

Isolate는 Flutter 앱에서 UI 응답성을 유지하면서 무거운 작업을 처리할 수 있는 강력한 도구입니다. 이미지 처리, 데이터 파싱, 복잡한 계산 작업 등을 메인 UI 스레드와 분리하여 수행함으로써 사용자 경험을 크게 향상시킬 수 있습니다.

하지만 Isolate는 만능 해결책이 아니며, 다음과 같은 경우에 특히 유용합니다:

  • CPU 집약적인 작업(계산, 파싱, 처리)
  • 1초 이상 소요되는 작업
  • UI 스레드를 막을 가능성이 있는 작업

간단한 작업이나 빠른 비동기 작업에는 일반 Futureasync/await이 더 적절할 수 있습니다. 항상 성능 측정을 통해 Isolate 사용 여부를 결정하는 것이 좋습니다.

results matching ""

    No results matching ""