Flutter Impeller 시대: 새 렌더러로 앱 성능 제대로 끌어올리기

모바일

FlutterImpeller성능 최적화Dart렌더링

이 글은 누구를 위한 것인가

  • Flutter 앱에서 첫 실행 시 버벅거림(Jank)을 경험한 개발자
  • Skia에서 Impeller로 마이그레이션하면서 렌더링 이슈가 생긴 팀
  • Flutter 앱의 크기를 줄이고 시작 속도를 개선하고 싶은 분

들어가며

Flutter 개발자들이 오랫동안 고통받던 문제가 있다. **쉐이더 컴파일 버벅거림(Shader Compilation Jank)**이다.

앱을 처음 실행할 때, 또는 새로운 화면을 처음 열 때 잠깐 멈추는 현상이다. 특히 복잡한 애니메이션이나 처음 보는 UI 요소가 등장할 때 0.5~1초 정도 화면이 뚝 끊기는 느낌이 든다.

원인은 Skia(이전 기본 렌더러)가 새로운 그래픽 쉐이더를 처음 만날 때 실시간으로 컴파일하기 때문이었다. 쉐이더 컴파일은 비싼 연산이고, 이것이 UI 스레드를 막아버렸다.

Flutter 팀이 3년 이상 개발한 Impeller가 이 문제를 근본적으로 해결했다. 쉐이더를 앱 빌드 시 미리 컴파일해두기 때문에, 런타임에 쉐이더 컴파일이 필요 없다.


1. Impeller vs Skia: 핵심 차이

항목SkiaImpeller
쉐이더 컴파일런타임 (처음 사용 시)빌드 타임 (미리 컴파일)
쉐이더 Jank있음 (특히 초기 실행)없음
성능안정적이지만 예측 불가더 일관된 프레임 시간
iOS 지원Flutter 3.0까지 기본Flutter 3.10부터 기본
Android 지원이전 기본값Flutter 3.16부터 기본
OpenGL 지원지원Metal(iOS), Vulkan(Android) 만

Impeller는 최신 그래픽 API(Metal, Vulkan)에 최적화되어 있어 구형 기기에서 지원이 제한될 수 있다. OpenGL만 지원하는 기기에서는 여전히 Skia를 사용한다.


2. Impeller 마이그레이션 시 발생하는 이슈들

Impeller가 기본값이 되면서 일부 앱에서 렌더링 차이가 발생했다. 자주 보고된 이슈들이다.

이슈 1: 그라디언트 렌더링 차이

// Skia에서는 자연스러웠던 그라디언트가
// Impeller에서 약간 다르게 보일 수 있음
Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.blue, Colors.purple],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  ),
)

// 해결: 색상 공간 명시적 지정
gradient: LinearGradient(
  colors: [Colors.blue, Colors.purple],
  // Impeller는 linear 색상 공간 사용
  // 필요 시 gamma-corrected 색상 직접 사용
)

이슈 2: 커스텀 페인터 호환성

CustomPainter에서 직접 Canvas API를 사용하는 경우 Impeller에서 다르게 보일 수 있다.

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // drawShadow, saveLayer 같은 일부 API는
    // Impeller에서 성능 특성이 다름

    // 대신 BoxShadow, 레이어 최소화 권장
    final paint = Paint()
      ..color = Colors.blue
      ..maskFilter = MaskFilter.blur(BlurStyle.normal, 10); // 블러 사용 시 성능 확인 필요
  }
}

이슈 3: 임시적으로 Skia로 롤백

이슈가 해결되기 전 임시 방편으로 Impeller를 비활성화할 수 있다.

# ios/Runner/Info.plist에 추가 (iOS)
<key>FLTEnableImpeller</key>
<false/>
<!-- android/app/src/main/AndroidManifest.xml (Android) -->
<meta-data
    android:name="io.flutter.embedding.android.EnableImpeller"
    android:value="false" />

단, 이는 임시 방편이다. Impeller가 Flutter의 미래 방향이므로 근본적인 해결을 권장한다.


3. Flutter DevTools로 성능 진단하기

Flutter DevTools의 Performance 탭이 성능 문제 진단의 핵심 도구다.

# DevTools 실행
flutter run --profile  # 프로파일 모드로 실행 (release에 가까운 성능)
# DevTools는 자동으로 열림 또는 flutter pub global run devtools

확인할 핵심 지표

지표정상경고
UI 스레드< 16ms> 16ms → Jank
Raster 스레드< 16ms> 16ms → 렌더링 병목
프레임 드롭률< 1%> 5% → 사용자 체감

UI 스레드 과부하 원인들

// 비싼 계산을 build() 안에서 하면 안 됨
Widget build(BuildContext context) {
  // ⚠️ 매 프레임 계산됨
  final filtered = items.where((e) => e.isActive).toList();
  final sorted = filtered.sort((a, b) => a.date.compareTo(b.date));

  return ListView(...);
}

// ✅ 상태가 바뀔 때만 계산
@override
void didUpdateWidget(covariant MyWidget oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.items != widget.items) {
    _prepareData();
  }
}

4. Dart Isolate로 UI 스레드 블로킹 방지

Dart는 기본적으로 단일 스레드(이벤트 루프 기반)다. CPU를 많이 쓰는 작업(이미지 처리, 데이터 파싱, 암호화)을 메인 스레드에서 하면 UI가 멈춘다.

Isolate는 Dart의 멀티스레딩 방식이다.

// 무거운 작업을 Isolate에서 처리
Future<List<ProcessedImage>> processImages(List<Uint8List> rawImages) async {
  // compute() = 간편한 Isolate 실행 API
  return await compute(_processImagesInBackground, rawImages);
}

// Isolate에서 실행될 함수 (최상위 함수 또는 static 메서드여야 함)
List<ProcessedImage> _processImagesInBackground(List<Uint8List> rawImages) {
  return rawImages.map((raw) {
    // 무거운 이미지 처리 로직
    return ProcessedImage(data: processRawImage(raw));
  }).toList();
}

JSON 파싱처럼 반복적인 작업도 데이터가 크면 Isolate로 처리하는 것이 안전하다.


5. 앱 번들 크기 줄이기

Flutter 앱은 초기 번들 크기가 크다는 단점이 있다. 최적화 방법들이다.

트리 쉐이킹

사용하지 않는 코드와 리소스가 자동으로 제거된다. 확인 방법:

flutter build apk --analyze-size
# 또는
flutter build ipa --analyze-size

Deferred Loading (지연 로딩)

자주 사용하지 않는 화면은 필요할 때 로드한다.

// 메인 앱
import 'package:myapp/features/settings/settings.dart' deferred as settings;

// 버튼 클릭 시
onPressed: () async {
  await settings.loadLibrary();  // 이때 다운로드
  Navigator.push(context, MaterialPageRoute(builder: (_) => settings.SettingsPage()));
}

특히 자주 사용하지 않는 기능(설정, 도움말, 고급 기능)에 적용하면 초기 로드 크기를 크게 줄일 수 있다.

이미지 최적화

// 플랫폼별 최적화 이미지 포맷 사용
// Android: WebP
// iOS: HEIC 또는 WebP

// 네트워크 이미지에 cached_network_image 사용
CachedNetworkImage(
  imageUrl: url,
  memCacheWidth: 300,  // 메모리에 300px 너비로 캐싱
  maxWidthDiskCache: 600,  // 디스크에 600px로 저장
)

6. 실제 서비스에서 Flutter 성능 개선 사례

사례: 배달 앱 목록 화면 최적화

문제: 음식점 목록 스크롤 시 프레임 드롭 발생 원인: 각 아이템에서 무거운 이미지 처리 + 실시간 거리 계산

해결:

  1. 거리 계산을 Isolate로 이동 → UI 스레드 부하 60% 감소
  2. RepaintBoundary로 각 리스트 아이템 격리 → Raster 비용 감소
  3. ListView.builder + const 생성자 적용
  4. Impeller 활성화 후 쉐이더 Jank 완전 제거

결과: 평균 프레임 시간 12ms → 7ms, 드롭 프레임 3% → 0.2%


맺으며

Impeller는 Flutter의 오랜 숙원 사업이었다. 쉐이더 컴파일 Jank가 없어지는 것만으로도 앱의 체감 품질이 크게 달라진다. 처음 실행할 때 버벅거리던 앱이 부드럽게 동작하기 시작한다.

Impeller 전환 후 렌더링 이슈가 생겼다면 대부분 해결 가능하다. Flutter GitHub의 Known Issues를 확인하고, 대부분 버전 업그레이드나 코드 수정으로 해결된다.

성능 최적화의 순서는 항상 같다. 측정하고, 병목을 찾고, 고치고, 다시 측정한다. DevTools 없이 감으로 최적화하는 것은 시간 낭비다. Flutter DevTools를 아직 안 써봤다면 오늘이 시작하기 좋은 날이다.