Flutter 튜토리얼 53편: Isolate와 동시성

Flutter Isolate와 동시성#

무거운 작업이 UI를 멈추게 하면 사용자 경험이 크게 저하됩니다. 이 튜토리얼에서는 Dart의 Isolate를 사용하여 백그라운드에서 작업을 처리하는 방법을 배웁니다.

학습 목표#

  • Isolate의 개념과 동작 원리 이해하기
  • Isolate.run()으로 간단한 백그라운드 작업 처리하기
  • 장기 실행 Isolate 구현하기
  • 플랫폼 플러그인과 Isolate 함께 사용하기

1. Isolate의 개념#

Why: Isolate가 필요한 이유#

Flutter 앱은 단일 스레드(UI 스레드)에서 실행됩니다. 만약 이 스레드에서 무거운 작업을 실행하면, 그 동안 UI가 멈춰버립니다. 사용자에게 앱이 “멈춤” 상태로 보이게 됩니다.

flowchart TD A[UI 스레드] --> B{무거운 작업} B -->|동기 실행| C[UI 멈춤 😰] B -->|Isolate 사용| D[UI 유지 😊] D --> E[별도 스레드에서 처리]

What: Isolate란?#

Isolate1는 Dart에서 동시성을 구현하는 방식입니다. 각 Isolate는 독립적인 메모리 공간을 가지며, 다른 Isolate와 메모리를 공유하지 않습니다.

flowchart LR subgraph Main["메인 Isolate"] A[이벤트 루프] B[메모리] C[UI 코드] end subgraph Worker["워커 Isolate"] D[이벤트 루프] E[메모리] F[무거운 작업] end Main <-->|메시지 전달| Worker
특징설명
독립 메모리각 Isolate는 별도의 메모리 힙을 가짐
메시지 전달데이터는 복사되어 전달됨
안전성메모리 공유 문제 없음
오버헤드생성 비용이 있음

What: 이벤트 루프와 프레임 갭#

Flutter는 이벤트 루프2를 통해 작업을 처리합니다. 각 프레임 사이에 약간의 유휴 시간(프레임 갭)이 있습니다.

sequenceDiagram participant F as 프레임 participant E as 이벤트 루프 participant G as 프레임 갭 F->>E: 프레임 1 렌더링 E->>G: 유휴 시간 (작업 처리) G->>F: 프레임 2 렌더링 F->>E: 반복...

짧은 비동기 작업은 이 프레임 갭에서 처리할 수 있습니다. 하지만 무거운 작업은 여러 프레임을 건너뛰게 만듭니다.

How: 언제 Isolate를 사용해야 하나요?#

상황Isolate 필요 여부이유
JSON 파싱 (작은 데이터)불필요프레임 갭에서 처리 가능
JSON 파싱 (큰 데이터)필요16ms 초과 가능
이미지 처리필요CPU 집약적 작업
암호화/복호화필요CPU 집약적 작업
파일 읽기불필요이미 비동기로 처리됨
HTTP 요청불필요이미 비동기로 처리됨

Watch out: 주의사항#

Isolate 생성에는 비용이 듭니다. 작은 작업에 Isolate를 사용하면 오히려 성능이 저하될 수 있습니다. 작업이 약 50ms 이상 걸릴 것으로 예상될 때 Isolate를 고려하세요.


2. Isolate.run() 사용하기#

Why: 가장 간단한 방법#

Isolate.run()은 Flutter 3.7/Dart 2.19에서 도입된 간편한 API입니다. 일회성 백그라운드 작업을 쉽게 처리할 수 있습니다.

What: Isolate.run()의 동작 방식#

sequenceDiagram participant M as 메인 Isolate participant W as 워커 Isolate M->>W: Isolate.run(함수) Note over W: 새 Isolate 생성 W->>W: 함수 실행 W->>M: 결과 반환 Note over W: Isolate 종료

How: 기본 사용법#

import 'dart:isolate';
// 무거운 작업을 수행하는 함수
int heavyComputation(int value) {
// 복잡한 계산 시뮬레이션
int result = 0;
for (int i = 0; i < value * 1000000; i++) {
result += i;
}
return result;
}
// Isolate에서 실행
Future<void> runInBackground() async {
final result = await Isolate.run(() {
return heavyComputation(100);
});
print('결과: $result');
}

How: JSON 파싱 예제#

큰 JSON 데이터를 파싱하는 실제 예제입니다:

import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/services.dart';
class Photo {
final int id;
final String title;
final String url;
Photo({
required this.id,
required this.title,
required this.url,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
);
}
}
Future<List<Photo>> getPhotos() async {
// 1. 메인 Isolate에서 파일 읽기 (rootBundle은 메인에서만 사용 가능)
final String jsonString = await rootBundle.loadString('assets/photos.json');
// 2. 워커 Isolate에서 파싱
final List<Photo> photos = await Isolate.run<List<Photo>>(() {
final List<dynamic> photoData = jsonDecode(jsonString) as List<dynamic>;
return photoData
.cast<Map<String, dynamic>>()
.map(Photo.fromJson)
.toList();
});
return photos;
}

How: 위젯에서 사용하기#

class PhotoListScreen extends StatefulWidget {
const PhotoListScreen({super.key});
@override
State<PhotoListScreen> createState() => _PhotoListScreenState();
}
class _PhotoListScreenState extends State<PhotoListScreen> {
List<Photo>? _photos;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadPhotos();
}
Future<void> _loadPhotos() async {
final photos = await getPhotos();
setState(() {
_photos = photos;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: _photos!.length,
itemBuilder: (context, index) {
final photo = _photos![index];
return ListTile(
title: Text(photo.title),
subtitle: Text(photo.url),
);
},
);
}
}

Watch out: 주의사항#

Isolate.run()에 전달하는 함수는 최상위 함수이거나 정적 메서드여야 합니다. 클래스의 인스턴스 메서드나 클로저는 사용할 수 없습니다.

// ❌ 잘못된 예: 인스턴스 메서드
class MyClass {
int value = 10;
Future<int> compute() async {
return await Isolate.run(() {
return value * 2; // 에러! value에 접근 불가
});
}
}
// ✅ 올바른 예: 값을 직접 전달
class MyClass {
int value = 10;
Future<int> compute() async {
final localValue = value; // 값 복사
return await Isolate.run(() {
return localValue * 2; // 복사된 값 사용
});
}
}

3. 장기 실행 Isolate#

Why: 반복 작업에 Isolate를 재사용해야 하는 이유#

Isolate.run()은 매번 새 Isolate를 생성하고 종료합니다. 반복적인 작업이 있다면 Isolate를 유지하고 재사용하는 것이 효율적입니다.

What: SendPort와 ReceivePort#

Isolate 간 통신은 포트3를 통해 이루어집니다.

flowchart LR subgraph Main["메인 Isolate"] A[ReceivePort] B[SendPort to Worker] end subgraph Worker["워커 Isolate"] C[ReceivePort] D[SendPort to Main] end B -->|메시지| C D -->|결과| A

How: 장기 실행 Isolate 구현#

import 'dart:isolate';
class BackgroundWorker {
late final Isolate _isolate;
late final SendPort _sendPort;
late final ReceivePort _receivePort;
bool _isReady = false;
// 워커 초기화
Future<void> initialize() async {
_receivePort = ReceivePort();
// Isolate 생성
_isolate = await Isolate.spawn(
_workerEntryPoint,
_receivePort.sendPort,
);
// 워커로부터 SendPort 받기
_sendPort = await _receivePort.first as SendPort;
_isReady = true;
}
// 워커 진입점 (최상위 함수)
static void _workerEntryPoint(SendPort sendPort) {
final receivePort = ReceivePort();
// 메인에 SendPort 전달
sendPort.send(receivePort.sendPort);
// 메시지 수신 대기
receivePort.listen((message) {
if (message is Map<String, dynamic>) {
final result = _processMessage(message);
sendPort.send(result);
}
});
}
// 메시지 처리 로직
static dynamic _processMessage(Map<String, dynamic> message) {
final type = message['type'] as String;
final data = message['data'];
switch (type) {
case 'compute':
return _heavyComputation(data as int);
case 'parse':
return _parseJson(data as String);
default:
return null;
}
}
static int _heavyComputation(int value) {
int result = 0;
for (int i = 0; i < value * 1000000; i++) {
result += i;
}
return result;
}
static List<Map<String, dynamic>> _parseJson(String json) {
// JSON 파싱 로직
return [];
}
// 작업 요청
Future<dynamic> compute(String type, dynamic data) async {
if (!_isReady) {
throw StateError('Worker not initialized');
}
final responsePort = ReceivePort();
_sendPort.send({
'type': type,
'data': data,
'responsePort': responsePort.sendPort,
});
return await responsePort.first;
}
// 정리
void dispose() {
_receivePort.close();
_isolate.kill();
}
}

How: 사용 예제#

class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _worker = BackgroundWorker();
@override
void initState() {
super.initState();
_worker.initialize();
}
@override
void dispose() {
_worker.dispose();
super.dispose();
}
Future<void> _doHeavyWork() async {
final result = await _worker.compute('compute', 100);
print('결과: $result');
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _doHeavyWork,
child: const Text('무거운 작업 실행'),
);
}
}

Watch out: 주의사항#

장기 실행 Isolate는 반드시 앱 종료 시 정리해야 합니다. dispose() 메서드에서 Isolate.kill()을 호출하지 않으면 메모리 누수가 발생합니다.


4. 플랫폼 플러그인과 Isolate#

Why: 플러그인을 Isolate에서 사용해야 하는 경우#

일부 무거운 작업은 플러그인 기능과 함께 사용해야 합니다. 예를 들어, 백그라운드에서 SharedPreferences에 접근하거나 네이티브 코드를 호출해야 할 수 있습니다.

What: BackgroundIsolateBinaryMessenger#

기본적으로 플랫폼 채널4은 메인 Isolate에서만 동작합니다. BackgroundIsolateBinaryMessenger를 사용하면 다른 Isolate에서도 플랫폼 플러그인을 사용할 수 있습니다.

flowchart TD A[메인 Isolate] --> B[RootIsolateToken 획득] B --> C[워커 Isolate에 전달] C --> D[BackgroundIsolateBinaryMessenger 초기화] D --> E[플러그인 사용 가능]

How: 설정 방법#

import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
// 메인 Isolate에서 RootIsolateToken 획득
final RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
// 워커 Isolate에 토큰 전달
Isolate.spawn(_isolateMain, rootIsolateToken);
}
Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
// BackgroundIsolateBinaryMessenger 초기화
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
// 이제 플랫폼 플러그인 사용 가능
final SharedPreferences prefs = await SharedPreferences.getInstance();
final bool? isDebug = prefs.getBool('isDebug');
print('isDebug: $isDebug');
}

How: 실제 사용 예제#

class BackgroundPrefsWorker {
late final Isolate _isolate;
late final SendPort _sendPort;
late final ReceivePort _receivePort;
Future<void> initialize() async {
_receivePort = ReceivePort();
final rootToken = RootIsolateToken.instance!;
_isolate = await Isolate.spawn(
_workerMain,
_WorkerConfig(
sendPort: _receivePort.sendPort,
rootToken: rootToken,
),
);
_sendPort = await _receivePort.first as SendPort;
}
static Future<void> _workerMain(_WorkerConfig config) async {
// 플러그인 초기화
BackgroundIsolateBinaryMessenger.ensureInitialized(config.rootToken);
final receivePort = ReceivePort();
config.sendPort.send(receivePort.sendPort);
await for (final message in receivePort) {
if (message is Map<String, dynamic>) {
final action = message['action'] as String;
final key = message['key'] as String;
final responsePort = message['responsePort'] as SendPort;
final prefs = await SharedPreferences.getInstance();
switch (action) {
case 'getString':
responsePort.send(prefs.getString(key));
break;
case 'setString':
final value = message['value'] as String;
await prefs.setString(key, value);
responsePort.send(true);
break;
}
}
}
}
Future<String?> getString(String key) async {
final responsePort = ReceivePort();
_sendPort.send({
'action': 'getString',
'key': key,
'responsePort': responsePort.sendPort,
});
return await responsePort.first as String?;
}
Future<void> setString(String key, String value) async {
final responsePort = ReceivePort();
_sendPort.send({
'action': 'setString',
'key': key,
'value': value,
'responsePort': responsePort.sendPort,
});
await responsePort.first;
}
void dispose() {
_receivePort.close();
_isolate.kill();
}
}
class _WorkerConfig {
final SendPort sendPort;
final RootIsolateToken rootToken;
_WorkerConfig({
required this.sendPort,
required this.rootToken,
});
}

Watch out: 주의사항#

모든 플러그인이 백그라운드 Isolate를 지원하는 것은 아닙니다. 플러그인 문서를 확인하고, 지원하지 않는 경우 메인 Isolate에서 처리해야 합니다.


5. Isolate의 제한사항#

Why: 제한사항을 알아야 하는 이유#

Isolate를 사용할 때 몇 가지 제한사항이 있습니다. 이를 모르면 런타임 오류가 발생할 수 있습니다.

What: 주요 제한사항#

flowchart TD A[Isolate 제한사항] --> B[rootBundle 접근 불가] A --> C[dart:ui 메서드 제한] A --> D[플랫폼 채널 기본 비활성화] A --> E[웹에서 다르게 동작]
제한사항설명해결 방법
rootBundle에셋에 직접 접근 불가메인에서 데이터 로드 후 전달
dart일부 메서드 사용 불가메인 Isolate에서 처리
플랫폼 채널기본적으로 비활성화BackgroundIsolateBinaryMessenger 사용
웹 플랫폼실제 병렬 처리 안됨웹 워커 또는 다른 접근 필요

How: rootBundle 제한 우회#

// ❌ 잘못된 예: Isolate에서 직접 rootBundle 접근
Future<List<Photo>> badExample() async {
return await Isolate.run(() async {
// 에러 발생!
final json = await rootBundle.loadString('assets/data.json');
return parsePhotos(json);
});
}
// ✅ 올바른 예: 메인에서 데이터 로드 후 전달
Future<List<Photo>> goodExample() async {
// 1. 메인 Isolate에서 파일 읽기
final json = await rootBundle.loadString('assets/data.json');
// 2. 워커 Isolate에서 파싱
return await Isolate.run(() {
return parsePhotos(json);
});
}

What: 웹 플랫폼의 특수성#

웹에서 Isolate.run()compute()메인 스레드에서 실행됩니다. 실제 병렬 처리가 필요하면 웹 워커를 사용해야 합니다.

import 'package:flutter/foundation.dart';
Future<List<Photo>> parsePhotosOptimal(String json) async {
if (kIsWeb) {
// 웹: 청크 단위로 나눠서 처리
return _parseInChunks(json);
} else {
// 네이티브: Isolate 사용
return await Isolate.run(() => parsePhotos(json));
}
}
List<Photo> _parseInChunks(String json) {
// 웹에서 UI 차단을 최소화하기 위한 처리
// 실제로는 웹 워커 사용을 권장
return parsePhotos(json);
}

Watch out: 주의사항#

웹 앱에서 무거운 작업이 있다면 웹 워커 사용을 고려하세요. Flutter의 기본 Isolate API는 웹에서 병렬 처리를 지원하지 않습니다.


6. 실전 패턴#

compute() 함수 활용#

compute()Isolate.run()보다 먼저 존재한 헬퍼 함수입니다.

import 'package:flutter/foundation.dart';
// compute 사용 예
Future<List<Photo>> parsePhotosWithCompute(String json) async {
return await compute(_parsePhotos, json);
}
// 최상위 함수로 정의
List<Photo> _parsePhotos(String json) {
final List<dynamic> data = jsonDecode(json) as List<dynamic>;
return data.cast<Map<String, dynamic>>().map(Photo.fromJson).toList();
}

작업 큐 패턴#

여러 작업을 순차적으로 처리하는 패턴입니다:

class TaskQueue {
final _queue = <Future<void> Function()>[];
bool _isProcessing = false;
void add(Future<void> Function() task) {
_queue.add(task);
_processNext();
}
Future<void> _processNext() async {
if (_isProcessing || _queue.isEmpty) return;
_isProcessing = true;
while (_queue.isNotEmpty) {
final task = _queue.removeAt(0);
await task();
}
_isProcessing = false;
}
}

마무리#

이번 튜토리얼에서는 Dart의 Isolate를 사용하여 무거운 작업을 백그라운드에서 처리하는 방법을 배웠습니다.

핵심 정리#

주제핵심 내용
Isolate 개념독립 메모리, 메시지 전달 방식
Isolate.run()일회성 작업에 적합
장기 실행SendPort/ReceivePort로 통신
플러그인 사용BackgroundIsolateBinaryMessenger 필요
제한사항rootBundle, dart, 웹 플랫폼

다음 단계#


참고 자료#

Footnotes#

  1. Isolate는 Dart의 동시성 모델로, 각각 독립된 메모리와 이벤트 루프를 가진 실행 단위입니다.

  2. 이벤트 루프(Event Loop)는 비동기 작업과 이벤트를 순차적으로 처리하는 메커니즘입니다.

  3. 포트(Port)는 Isolate 간 메시지를 주고받는 통신 채널입니다. SendPort는 보내기, ReceivePort는 받기용입니다.

  4. 플랫폼 채널(Platform Channel)은 Flutter와 네이티브 코드(Android/iOS) 간 통신을 위한 메커니즘입니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 53편: Isolate와 동시성
https://moodturnpost.net/posts/flutter/flutter-performance-isolates/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차