Isolate — 멀티스레드 연산으로 UI 버벅임 해결

Dart는 단일 스레드이지만, Isolate를 사용하면 별도의 스레드에서 무거운 연산을 실행할 수 있습니다. 이미지 처리, JSON 파싱, 암호화 등 CPU 집약적인 작업에서 UI가 버벅이는 문제를 해결합니다.


왜 Isolate가 필요한가?

DART
// 이렇게 하면 UI가 멈춤!
void _onButtonPressed() {
  final result = heavyComputation();  // 3초 걸리는 작업
  setState(() => _result = result);    // 3초 동안 UI 정지
}

Dart의 메인 스레드는 UI 렌더링과 이벤트 처리를 담당합니다. 여기서 무거운 작업을 하면 60fps를 유지할 수 없어 화면이 버벅입니다.

면접 포인트: "Dart는 단일 스레드인데 Isolate는 뭔가요?"라는 질문이 자주 나옵니다. Isolate는 독립된 메모리 공간을 가진 별도의 실행 단위 입니다. 스레드와 달리 메모리를 공유하지 않아 동기화 문제가 없습니다.


Isolate.run — 가장 간단한 방법

DART
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가 제공하는 편의 함수로, 단일 값을 전달하고 결과를 받습니다.

DART
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 파싱은 메인 스레드를 블로킹할 수 있습니다.

DART
// 최상위 함수로 파싱 로직 정의
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가 필요한 경우, 매번 새로 생성하는 것보다 하나를 유지하는 것이 효율적입니다.

DART
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의 특성

PLAINTEXT
┌──────────────┐     메시지 전달     ┌──────────────┐
│  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를 유지하세요
댓글 로딩 중...