Flutter 성능의 비밀: Dart Event Loop와 Isolate 깊이 파헤치기

Flutter 앱을 개발하다 보면 공통으로 마주하는 질문이 있습니다. “Dart는 싱글 스레드라는데, 어떻게 비동기 처리를 이렇게 매끄럽게 할까?” 그리고 “데이터가 너무 많아서 화면이 버벅거리는데(Jank), 이걸 어떻게 해결하지?” 오늘은 이 질문들에 대한 답을 찾기 위해 Dart의 심장부인 **Event Loop**와 병렬 처리의 핵심인 **Isolate**를 심층 분석해 보겠습니다.

Dart Event Loop

[그림 1] Dart의 비동기 처리 메커니즘: Event Loop와 큐(Queue)의 구조

1. Event Loop: 싱글 스레드의 마법

Dart는 기본적으로 **싱글 스레드** 언어입니다. 즉, 한 번에 하나의 작업만 수행할 수 있죠. 하지만 우리는 `Future`, `async/await`를 통해 여러 작업을 동시에 하는 것처럼 느낍니다. 그 비결이 바로 **Event Loop**입니다.

Event Loop는 두 개의 큐를 관리합니다:
– **Microtask Queue**: 가장 높은 우선순위를 가집니다. 현재 실행 중인 작업이 끝나면 ‘즉시’ 실행되어야 하는 짧은 작업들이 들어갑니다.
– **Event Queue**: 외부 이벤트(I/O, 타이머, 클릭 등)나 `Future`가 처리되는 곳입니다.

중요한 점은 Microtask Queue가 완전히 비워지기 전까지는 Event Queue의 작업이 시작되지 않는다는 것입니다. 이 순서를 잘못 이해하면 UI 업데이트가 지연되는 현상을 겪을 수 있습니다.

2. Isolate: 독립적인 실행 단위

싱글 스레드인 메인 Isolate에서 무거운 연산(예: 수만 개의 JSON 파싱, 복잡한 이미지 처리)을 수행하면 어떻게 될까요? Event Loop가 그 작업에 붙잡혀 UI를 그리지 못하게 되고, 사용자는 화면이 멈춘 것 같은 불쾌한 경험(Jank)을 하게 됩니다.

Dart Isolates

[그림 2] 서로 다른 메모리 영역을 가진 독립적인 Isolate 모델

이때 필요한 것이 **Isolate**입니다. 일반적인 멀티 스레드와 달리, Dart의 Isolate는 메모리를 공유하지 않습니다. 덕분에 **’Lock’이나 ‘값의 오염’ 걱정 없이 안전하게 병렬 처리**를 할 수 있습니다. 각 Isolate는 자신만의 메모리와 Event Loop를 가진 완전한 독립체이며, 서로 ‘메시지’를 주고받으며 통신합니다.

3. 실전: compute로 UI 버벅임 해결하기

UI Jank vs Smooth

[그림 3] 무거운 작업을 Isolate로 분리했을 때의 극적인 UI 성능 체감 차이

Flutter에서는 `compute` 함수를 통해 아주 쉽게 작업을 다른 Isolate로 보낼 수 있습니다.

// [Bad] 메인 스레드를 멈추게 하는 코드
final data = heavyJsonParsing(jsonString);

// [Good] 새로운 Isolate에서 실행하여 UI를 부드럽게 유지
final data = await compute(heavyJsonParsing, jsonString);

위의 `compute`는 내부적으로 별도의 Isolate를 생성하고, 작업이 끝나면 결과를 메인 Isolate로 돌려준 뒤 스스로 소멸합니다. 단순한 한 줄의 코드 추가만으로 위 그림과 같이 사용자의 ‘분노’를 ‘만족’으로 바꿀 수 있습니다.

마치며: 효율적인 성능 최적화의 첫걸음

모든 비동기 처리가 병렬 처리는 아닙니다. 대부분의 네트워크 요청은 `Future`만으로 충분하지만, CPU 연산량이 많은 작업은 반드시 `Isolate`를 고려해야 합니다. Dart의 동작 원리를 정확히 이해하고 적재적소에 Isolate를 배치한다면, 여러분의 Flutter 앱은 그 어떤 복잡한 로직 속에서도 ’60fps (또는 120fps)’의 부드러움을 잃지 않을 것입니다.

댓글 남기기