Isolate — 멀티스레드 연산으로 UI 버벅임 해결
Isolate — 멀티스레드 연산으로 UI 버벅임 해결
Dart는 단일 스레드이지만, Isolate를 사용하면 별도의 스레드에서 무거운 연산을 실행할 수 있습니다. 이미지 처리, JSON 파싱, 암호화 등 CPU 집약적인 작업에서 UI가 버벅이는 문제를 해결합니다.
왜 Isolate가 필요한가?
// 이렇게 하면 UI가 멈춤!
void _onButtonPressed() {
final result = heavyComputation(); // 3초 걸리는 작업
setState(() => _result = result); // 3초 동안 UI 정지
}
Dart의 메인 스레드는 UI 렌더링과 이벤트 처리를 담당합니다. 여기서 무거운 작업을 하면 60fps를 유지할 수 없어 화면이 버벅입니다.
면접 포인트: "Dart는 단일 스레드인데 Isolate는 뭔가요?"라는 질문이 자주 나옵니다. Isolate는 독립된 메모리 공간을 가진 별도의 실행 단위 입니다. 스레드와 달리 메모리를 공유하지 않아 동기화 문제가 없습니다.
Isolate.run — 가장 간단한 방법
import 'dart:isolate';
// 무거운 연산을 Isolate에서 실행
Future<int> computeInBackground() async {
return await Isolate.run(() {
// 이 코드는 별도의 Isolate에서 실행됨
int sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
});
}
// UI에서 사용
void _calculate() async {
setState(() => _isLoading = true);
final result = await computeInBackground();
setState(() {
_result = result;
_isLoading = false;
});
}
compute 함수 (Flutter 제공)
compute는 Flutter가 제공하는 편의 함수로, 단일 값을 전달하고 결과를 받습니다.
import 'package:flutter/foundation.dart';
// 최상위 함수 또는 static 메서드여야 함
int _fibonacci(int n) {
if (n <= 1) return n;
return _fibonacci(n - 1) + _fibonacci(n - 2);
}
// 사용
final result = await compute(_fibonacci, 40);
compute의 제약
- 전달할 함수는 최상위 함수 또는 static 메서드 여야 합니다
- 클로저(캡처된 변수가 있는 함수)는 사용할 수 없습니다
- 인자와 반환값은 직렬화 가능해야 합니다
실전: JSON 파싱
대용량 JSON 파싱은 메인 스레드를 블로킹할 수 있습니다.
// 최상위 함수로 파싱 로직 정의
List<Post> _parsePosts(String jsonString) {
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => Post.fromJson(json)).toList();
}
// API 호출 + 파싱을 분리
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('$baseUrl/posts'));
if (response.statusCode == 200) {
// 파싱을 Isolate에서 실행
return compute(_parsePosts, response.body);
}
throw Exception('API 호출 실패');
}
장기 실행 Isolate (SendPort/ReceivePort)
반복적으로 Isolate가 필요한 경우, 매번 새로 생성하는 것보다 하나를 유지하는 것이 효율적입니다.
class BackgroundWorker {
late final Isolate _isolate;
late final SendPort _sendPort;
final _receivePort = ReceivePort();
Future<void> start() async {
_isolate = await Isolate.spawn(
_workerEntryPoint,
_receivePort.sendPort,
);
// Worker의 SendPort 받기
_sendPort = await _receivePort.first as SendPort;
}
Future<dynamic> execute(dynamic message) async {
final responsePort = ReceivePort();
_sendPort.send([message, responsePort.sendPort]);
return await responsePort.first;
}
void stop() {
_isolate.kill(priority: Isolate.immediate);
_receivePort.close();
}
// Worker Isolate의 진입점
static void _workerEntryPoint(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
mainSendPort.send(workerReceivePort.sendPort);
workerReceivePort.listen((message) {
final data = message[0];
final replyPort = message[1] as SendPort;
// 무거운 작업 수행
final result = _processData(data);
replyPort.send(result);
});
}
static dynamic _processData(dynamic data) {
// 실제 처리 로직
return data;
}
}
// 사용
final worker = BackgroundWorker();
await worker.start();
final result = await worker.execute(someData);
worker.stop();
Isolate의 특성
┌──────────────┐ 메시지 전달 ┌──────────────┐
│ Main Isolate │ ◄──────────────► │ Worker │
│ (UI 스레드) │ SendPort/ │ Isolate │
│ │ ReceivePort │ │
│ - 자체 힙 │ │ - 자체 힙 │
│ - 자체 이벤트 │ │ - 자체 이벤트 │
│ 루프 │ │ 루프 │
└──────────────┘ └──────────────┘
- 각 Isolate는 독립된 메모리 를 가집니다 (힙 공유 없음)
- 통신은 **메시지 전달 **(SendPort/ReceivePort)로만 가능
- 전달할 데이터는 ** 직렬화 가능 **해야 합니다
- Isolate 간에 객체 참조를 공유할 수 없습니다
언제 Isolate를 사용해야 할까?
| 작업 | Isolate 필요? |
|---|---|
| API 호출 (네트워크 I/O) | 불필요 (async/await) |
| 대용량 JSON 파싱 | 필요 |
| 이미지 리사이징/필터링 | 필요 |
| 암호화/복호화 | 필요 |
| 파일 읽기/쓰기 | 보통 불필요 |
| 복잡한 수학 연산 | 필요 |
핵심: CPU 집약적 인 작업에만 Isolate를 사용하세요. I/O 바운드 작업은 async/await로 충분합니다.
정리
- Isolate는 독립된 메모리를 가진 별도의 실행 단위입니다 (스레드와 다름)
Isolate.run()또는compute()로 간단하게 백그라운드 연산을 실행합니다- CPU 집약적인 작업(파싱, 이미지 처리, 암호화)에 사용하세요
- I/O 작업(네트워크, 파일)은 async/await로 충분합니다
- 장기 실행이 필요하면 SendPort/ReceivePort로 통신하는 Isolate를 유지하세요
댓글 로딩 중...