[Flutter] Isolate를 사용하여 큰 작업 분리하기

민태호·2025년 7월 12일
0

Flutter

목록 보기
21/23
post-thumbnail

우리가 보통 소요시간이 있거나, 서버를 통해 수행하는 작업은 동기적(synchronous)으로 처리하기 어렵기 때문에 비동기적(asynchronous)으로 처리하여 그 사이에 미리 UI를 그리거나 다른 작업을 할 수 있도록 한다.
하지만 큰 작업을 실행할 경우, 화면이 멈추는 현상이 발생한다.

왜일까?

Flutter가 사용하는 Dart언어는 싱글스레드로 작동한다

처리하는 모든 작업이 하나의 스레드에서 동작하기 때문에, 아무리 비동기 작업이라고 해도 UI에 영향을 주는 것이다.

그렇다면, 메인 스레드와 완전히 독립적으로 작업을 실행할 수 없을까?

Flutter는 모든 작업이 메인Isolate(메인 스레드를 말하는 것 같다)에서 실행되며, 큰 작업을 별도의 Isolate에 할당할 수 있다고 설명한다. https://docs.flutter.dev/perf/isolates
Isolate는 각각 자체 메모리와 자체 이벤트 루프를 가지고 있으며, 상태를 공유하지 않고 메시지를 통해서만 통신할 수 있다고 한다.

아래 구현 코드에서 차이를 눈으로 확인할 수 있다.

구현 코드 - async

우선 큰 작업을 실행하는 비동기 함수를 작성해준다.
index변수 3개를 각각 10억번 씩 증가시키는 작업으로, 아주 작은 작업이 30억번 반복되는 것이다.

Future<void> bigTask() async {
  int index = 0;
  int index2 = 0;
  int index3 = 0;

  while (index < 1000000000) {
    index++;
  }

  while (index2 < 1000000000) {
    index2++;
  }

  while (index3 < 1000000000) {
    index3++;
  }
}

작업이 끝나면 상태를 업데이트하고, 상태에 따라 "완료" 텍스트가 표시되도록 UI를 구성한다.
Column(
  spacing: 10,
  mainAxisSize: MainAxisSize.min,
  children: [
    CircularProgressIndicator(),
    Text("async", style: TextStyle(fontSize: 30)),
    isAsyncTaskDone
        ? Text("완료", style: TextStyle(fontSize: 40))
        : CircularProgressIndicator(),
    FilledButton(
      onPressed: () async {
        setState(() => isAsyncTaskDone = false);
        await bigTask(); // 큰 작업 함수
        setState(() => isAsyncTaskDone = true); // 완료 후 상태 변경
      },
      child: Text("실행"),
    ),
  ],
)

구현 코드 - isolate

이제 isolate를 사용한 코드이다.
큰 작업을 실행하는 비동기 함수를 작성할 때, SendPort를 이용해서 메시지를 주고받을 수 있도록 해준다. 위에서 설명했듯이 별도의 Isolate로 분리한 작업은 메시지를 통해서만 통신할 수 있기 때문이다.
작업이 끝나면 true값을 보내도록 간단하게 구현해준다.
sendPort.send() 함수를 뜯어보면 인자를 Object로 받기 때문에, 웬만한 값은 모두 넘길 수 있다.

Future<void> isolatedBigTask(SendPort sendPort) async {
  int index = 0;
  int index2 = 0;
  int index3 = 0;

  while (index < 1000000000) {
    index++;
  }

  while (index2 < 1000000000) {
    index2++;
  }

  while (index3 < 1000000000) {
    index3++;
  }

  sendPort.send(true);
}

구현 과정은 다음과 같다.

  • SendPort를 사용하기 위해 ReceivePort를 생성한다.
  • Isolate로 작업 분리 실행 시, Isolate.spawn()을 사용한다.
  • 1번 인자로 큰 작업 함수를, 2번 인자로는 큰 작업 함수에 넘길 인자를 포함한다. (여기서는 SendPort)
  • 모든 작업이 끝나고 작업이 실행된 Isolate를 제거해야 하기 때문에, 실행 시 await를 사용하여 인스턴스를 담아둬야 한다.
  • receivePort.listen()을 사용하여 넘어오는 값을 리스닝한다.
    작업이 끝날 경우, IsolateReceivePort를 전부 해제하고 닫아준다.
// 버튼 부분만 작성함
FilledButton(
  onPressed: () async {
    ReceivePort receivePort = ReceivePort(); // ReceivePort 생성

    setState(() => isIsolateTaskDone = false);

	// isolate로 실행
    _isolate = await Isolate.spawn(
      isolatedBigTask, // 함수
      receivePort.sendPort, // 인자
    );

	// listening
    receivePort.listen((message) {
      if (!mounted) {
        return;
      }

  	  // 넘어오는 값이 boolean형일 경우
      if (message is bool) {
        
        // true라면
        if (message) {
          setState(() {
            isIsolateTaskDone = true; // 상태 변경
            _isolate?.kill(); // isolate 중지
            _isolate = null; // isolate 비우기
            receivePort.close(); // ReceivePort 닫기
          });
        }
      }
    });
  },
  child: Text("실행"),
)

실행 화면

비동기 실행 모습isolate 사용 모습

실행 화면에서 볼 수 있듯이 비동기 함수만을 이용하여 큰 작업을 처리하면 UI가 멈추는 현상이 발생하는 반면, isolate를 사용하여 작업을 분리하면 UI 멈춤 없이 작업을 실행할 수 있다.

Github : https://github.com/mintaeh0/isolate_example

profile
Flutter Developer

0개의 댓글