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

[그림 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)을 하게 됩니다.

[그림 2] 서로 다른 메모리 영역을 가진 독립적인 Isolate 모델
이때 필요한 것이 **Isolate**입니다. 일반적인 멀티 스레드와 달리, Dart의 Isolate는 메모리를 공유하지 않습니다. 덕분에 **’Lock’이나 ‘값의 오염’ 걱정 없이 안전하게 병렬 처리**를 할 수 있습니다. 각 Isolate는 자신만의 메모리와 Event Loop를 가진 완전한 독립체이며, 서로 ‘메시지’를 주고받으며 통신합니다.
3. 실전: compute로 UI 버벅임 해결하기

[그림 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)’의 부드러움을 잃지 않을 것입니다.