수년간 프로덕션 레벨에서 Flutter 앱을 배포하며 가장 뼈아팠던 순간은 복잡한 애니메이션이 처음 실행될 때 발생하는 미세한 끊김, 바로 'Jank' 현상이었다. 우리는 이를 해결하기 위해 '셰이더 웜업(Shader Warm-up)'이라는 꼼수를 써야 했지만, 이는 앱 초기 구동 속도를 희생시키는 미봉책일 뿐이었다. 이제 구글은 Impeller라는 새로운 렌더링 엔진을 통해 이 문제를 근본적으로 제거하려 한다. 스키아(Skia)가 왜 실패했는지, 그리고 임펠러가 어떻게 120Hz의 부드러움을 보장하는지 엔지니어링 관점에서 분석한다.
스키아(Skia)의 배신: Shader Compilation Jank
Skia는 훌륭한 2D 그래픽 라이브러리다. 크롬과 안드로이드의 근간이 되는 기술이지만, Flutter의 렌더링 방식과는 태생적인 불일치가 존재했다. 문제는 '동적 셰이더 컴파일'에서 발생한다.
이 현상은 특히 iOS의 Metal API 전환 이후 더욱 두드러졌다. 자세한 내용은 Flutter 공식 성능 문서에서도 웜업 전략으로 언급된 바 있다. 하지만 임펠러는 접근 방식 자체가 다르다.
임펠러(Impeller)의 핵심: AOT 셰이더 컴파일
Impeller의 존재 이유는 명확하다. "런타임에는 절대 셰이더를 컴파일하지 않는다." 임펠러는 앱 빌드 타임에 필요한 모든 셰이더를 미리 컴파일(AOT - Ahead of Time)한다.
이것이 가능한 이유는 임펠러가 Flutter의 UI 레이어와 밀접하게 결합하여, 사용될 셰이더의 조합을 미리 예측하고 생성할 수 있도록 설계되었기 때문이다. iOS에서는 Metal Shading Language (MSL)로, 안드로이드에서는 SPIR-V로 변환되어 패키징된다.
// 기존 Skia 방식 (의사 코드)
void drawRect() {
if (!shader_compiled) {
compileShader(); // 여기서 프레임 드랍 발생!
}
gpu.draw();
}
// Impeller 방식
void drawRect() {
// 이미 빌드 타임에 바이너리로 존재함
gpu.bindPipeline(precompiled_pipeline);
gpu.draw(); // 즉시 실행
}
이 아키텍처 덕분에 예측 불가능한 렉이 사라지고, 일정한 프레임 레이트를 보장할 수 있게 되었다. 이는 단순한 최적화가 아니라, 렌더링 파이프라인의 재설계다.
성능 벤치마크 및 적용 현황
임펠러는 현재 iOS에서 기본값으로 활성화되어 있으며, 안드로이드(Vulkan 지원 기기)에서도 프로덕션 사용이 권장되는 단계로 진입했다. 아래는 일반적인 리스트 스크롤 시나리오에서의 프레임 타임 비교다.
| 지표 | Skia (Legacy) | Impeller (New) |
|---|---|---|
| Worst Frame Time | ~120ms (Jank 발생) | ~14ms (안정적) |
| Shader Compile | 런타임 (JIT) | 빌드타임 (AOT) |
| Backend API | OpenGL / Metal Wrapper | Metal / Vulkan Direct |
| 메모리 사용량 | 높음 (캐싱 필요) | 최적화됨 |
AndroidManifest.xml에 <meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="true" />를 추가하거나 런타임 플래그 --enable-impeller를 사용한다.
결론
Flutter 팀이 스키아를 버리고 자체 엔진인 Impeller를 개발한 것은 막대한 엔지니어링 리소스를 투입한 도박이었으나, 결과적으로 옳은 결정이었다. 이제 우리는 셰이더 웜업 코드를 삭제할 수 있게 되었으며, 디자이너가 요구하는 화려한 블러(Blur)와 복잡한 애니메이션을 성능 저하 걱정 없이 구현할 수 있다. 아직 안드로이드 Vulkan 미지원 기기에 대한 과제가 남아있지만, 플러터의 미래는 확실히 임펠러 위에 세워지고 있다. 지금 바로 프로덕션 빌드 로그를 확인하여 임펠러가 활성화되어 있는지 점검하라.
Post a Comment