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);
}
위 코드에서:
ReceivePort
를 생성하여 메시지를 수신할 채널을 엽니다.Isolate.spawn
을 사용하여 새 Isolate를 생성하고, 실행할 함수와 통신을 위한SendPort
를 전달합니다.- 새 Isolate에서 무거운 계산을 수행하고 결과를 메인 Isolate로 전송합니다.
- 메인 Isolate는
receivePort.first
를 통해 결과를 기다립니다.
3. 양방향 통신 구현하기
Isolate와의 양방향 통신을 구현하려면 양쪽 모두 ReceivePort
와 SendPort
가 필요합니다:
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 스레드를 막을 가능성이 있는 작업
간단한 작업이나 빠른 비동기 작업에는 일반 Future
나 async/await
이 더 적절할 수 있습니다. 항상 성능 측정을 통해 Isolate 사용 여부를 결정하는 것이 좋습니다.