2026년 Flutter 성능 최적화: Impeller, 리빌드, 모범 사례

2026년에 Flutter 앱을 60 또는 120fps로 안정적으로 유지하는 방법. Impeller, 절제된 위젯 리빌드, RepaintBoundary, DevTools 프로파일링을 다룹니다.

Impeller와 줄어든 위젯 리빌드를 활용한 Flutter 성능 최적화

2026년 Flutter 성능 최적화는 부드러운 앱을 만드는 방식을 바꾼 두 가지 전환에 기대고 있습니다. 렌더링 엔진 Impeller가 iOS와 Android에서 기본값으로 제공되기 시작했다는 점, 그리고 프레임워크의 리빌드 모델이 위젯 트리를 작고 불변으로 유지하는 개발자에게 보상을 준다는 점입니다. 이 두 지렛대에 기대는 팀은 프레임마다 손으로 조정하지 않고도 60 또는 120fps를 안정적으로 유지합니다.

가장 영향이 큰 변화

대부분의 Flutter 앱에서 가장 큰 이득은 const 생성자를 추가하고 큰 build 메서드를 분할하는 것입니다. 그러면 setState 호출이 화면 전체가 아니라 버튼 하나만 다시 그립니다.

Impeller가 Flutter 성능의 기준선을 다시 세우는 이유

Impeller는 Flutter의 렌더링 엔진이며, 모바일 플랫폼 전반에서 Skia를 대신해 기본값이 되었습니다. 차이가 가장 크게 드러나는 곳은 애니메이션의 처음 몇 초입니다. Skia는 셰이더를 저스트 인 타임으로, 특정 효과가 화면에 처음 나타날 때 컴파일했고, 이것이 Flutter 앱이 비판받던 그 악명 높은 첫 실행 끊김을 만들어 냈습니다. Impeller는 그 셰이더들을 빌드 시점에 미리 컴파일하므로, 애니메이션의 맨 첫 프레임이 100번째 프레임만큼 부드럽습니다.

Impeller는 또한 최신 그래픽 API를 직접 겨냥합니다. iOS에서는 Metal, Android에서는 Vulkan을 사용하며, 더 오래된 Android 하드웨어를 위한 OpenGL 폴백을 제공합니다. GitHub의 Impeller 엔진 문서는 렌더링 아키텍처와 플랫폼 지원 매트릭스를 상세히 설명합니다.

측면Skia(레거시)Impeller(2026 기본값)
셰이더 컴파일런타임, 첫 실행 끊김을 유발사전, 빌드 시점
iOS 백엔드OpenGL / MetalMetal
Android 백엔드OpenGLVulkan, OpenGL 폴백
첫 애니메이션의 부드러움이따금 끊김일관됨

대부분의 프로젝트에서 마이그레이션은 공짜입니다. 최근 Flutter 릴리스는 Impeller를 자동으로 활성화하고, 옛 셰이더 워밍업 우회책(--purge-persistent-cache, 번들된 .sksl 파일)은 더 이상 필요 없어 삭제할 수 있습니다. Flutter가 지원하는 GLSL 하위 집합에 맞춰 작성된 사용자 지정 FragmentProgram 셰이더도 그대로 이어집니다. Impeller가 이를 프레임워크 자체 효과와 동일한 오프라인 파이프라인으로 컴파일하기 때문입니다.

앱이 어느 엔진에서 실행되는지 확인하는 데는 한 줄이면 됩니다. 디버그 빌드는 시작 시 활성 백엔드를 출력하고, DevTools 인스펙터는 앱의 렌더링 세부 정보에서 이를 보고합니다. Impeller가 확인되면, 남은 성능 작업은 위젯 계층으로 올라갑니다. 그곳에서 프레임워크는 화면이 실제로 바뀌는 것보다 훨씬 자주 리빌드합니다. 이 가이드의 나머지가 초점을 맞추는 곳이 바로 그곳입니다. 빠른 엔진도 초당 수백 번 스스로를 다시 그리는 위젯 트리는 구할 수 없기 때문입니다.

const와 위젯 분할로 Flutter 리빌드 줄이기

모든 setState는 자신의 위젯을 오래된 것으로 표시하고 그 요소와 하위 트리에 대해 build를 다시 실행합니다. 요령은 그 하위 트리를 가능한 한 좁게 유지하는 것입니다. 두 가지 습관이 작업의 대부분을 담당합니다. 정적 위젯을 const로 표시하는 것, 그리고 상태를 아래로 밀어 상태를 가진 부분을 작게 유지하는 것입니다.

const 위젯은 단일 인스턴스로 정규화되므로, 부모가 리빌드될 때 Flutter는 동일한 참조를 비교하고, 변화가 없다고 판단하여, 그 가지의 다시 그리기를 완전히 건너뜁니다.

product_screen.dartdart
// A cart update repaints the badge, not the header.

class ProductScreen extends StatefulWidget {
  const ProductScreen({super.key});

  
  State<ProductScreen> createState() => _ProductScreenState();
}

class _ProductScreenState extends State<ProductScreen> {
  int _cartCount = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ProductHeader(),         // const: never rebuilds on setState
        CartBadge(count: _cartCount),   // only this subtree rebuilds
        FilledButton(
          onPressed: () => setState(() => _cartCount++),
          child: const Text('Add to cart'),
        ),
      ],
    );
  }
}

analysis_options.yaml에서 prefer_const_constructors 린트를 활성화하면 이 규율이 컴파일러 검사로 바뀌므로, const 기회가 조용히 사라지는 일이 없습니다. 관련 규칙 prefer_const_literals_to_create_immutables는 위젯에 전달되는 리스트와 맵에도 같은 보장을 확장합니다.

위젯 키도 여기서 언급할 가치가 있습니다. 리스트의 순서가 바뀌거나 위젯의 타입은 같지만 정체성이 바뀔 때, ValueKeyObjectKey는 Flutter가 상태를 헐고 다시 만드는 대신 이전 요소를 올바른 새 위젯 인스턴스에 매칭하도록 해 줍니다. 키의 부재는 동적 리스트에서 스크롤 위치가 사라지고 애니메이션이 리셋되는 흔한 원인이며, 그로 인한 추가 리빌드 작업은 놓치기 쉽습니다.

바뀐 데이터로 리빌드 범위 좁히기

상태가 트리의 높은 곳에 있으면, 잘 분할된 레이아웃이라도 여전히 많이 리빌드됩니다. 해법은 값을 브로드캐스트하고 위젯 하나만 그것을 듣게 하는 것입니다. Flutter는 바로 이를 위해 ValueNotifierValueListenableBuilder를 제공하며, 외부 패키지는 필요 없습니다.

article_list.dartdart
// A scroll-to-top button that rebuilds alone, not the whole page.

class ArticleList extends StatefulWidget {
  const ArticleList({super.key});

  
  State<ArticleList> createState() => _ArticleListState();
}

class _ArticleListState extends State<ArticleList> {
  final _showFab = ValueNotifier<bool>(false);
  final _controller = ScrollController();

  
  void initState() {
    super.initState();
    // Update the notifier, not setState: the list never rebuilds
    _controller.addListener(() => _showFab.value = _controller.offset > 400);
  }

  
  void dispose() {
    _showFab.dispose();
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _controller,
        itemCount: 200,
        itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
      ),
      floatingActionButton: ValueListenableBuilder<bool>(
        valueListenable: _showFab,
        builder: (_, visible, __) => visible
            ? FloatingActionButton(
                onPressed: () => _controller.jumpTo(0),
                child: const Icon(Icons.arrow_upward),
              )
            : const SizedBox.shrink(),
      ),
    );
  }
}

상태 관리 라이브러리는 같은 아이디어를 일반화합니다. Riverpod의 ref.watch(provider.select(...))와 Provider의 Selector 위젯은 모두 상태의 선택된 조각이 바뀔 때만 리빌드하며, 이것이 데이터가 많은 화면에서 부드러움과 버벅임을 가르는 결정적 요인입니다. 이 라이브러리들 사이의 절충은 2026년 Flutter 상태 관리 가이드에서 다룹니다.

리빌드 범위에 주의

화면의 루트 근처에서 setState를 호출하거나 페이지 전체를 최상위 Consumer로 감싸면, 변경마다 조용히 수백 개의 위젯이 다시 그려집니다. 어떤 화면이 저렴하다고 단정하기 전에 DevTools의 리빌드 카운터로 프로파일하십시오.

Flutter 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

RepaintBoundary로 비싼 다시 그리기 격리하기

리빌드와 다시 그리기는 별개의 단계입니다. 위젯은 리빌드를 건너뛰고도, 소란스러운 이웃이 레이어를 공유하기 때문에 다시 그려질 수 있습니다. RepaintBoundary는 하위 트리에 자체 레이어를 부여하므로, 애니메이션되는 차트가 정적 카드에 매 프레임 다시 그리도록 강요할 수 없습니다.

dashboard.dartdart
// The live chart repaints on its own layer, cards stay untouched.

class Dashboard extends StatelessWidget {
  const Dashboard({required this.ticker, super.key});
  final Stream<double> ticker;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // SummaryCards is static, so isolate it from the animation below
        const RepaintBoundary(child: SummaryCards()),
        RepaintBoundary(
          child: StreamBuilder<double>(
            stream: ticker,
            builder: (_, snap) => LivePriceChart(value: snap.data ?? 0),
          ),
        ),
      ],
    );
  }
}

RepaintBoundary는 공짜가 아닙니다. 각각이 레이어 하나를 할당하므로 여기저기 뿌리면 도움보다 해가 큽니다. 정말로 애니메이션되는 영역과 긴 스크롤 리스트 주위에 사용하고, 타임라인에서 이득을 확인하십시오. RepaintBoundary API 레퍼런스는 디버그 다시 그리기 오버레이가 실제로 다시 그려지는 레이어를 어떻게 강조하는지 문서화합니다.

build 메서드를 저렴하고 부작용 없이 유지하기

build 메서드는 리빌드마다 실행되므로, 그 안의 비싼 것은 무엇이든 배가됩니다. 규칙은 build가 상태를 읽고 위젯을 반환하는 것, 그 이상은 아니라는 것입니다. build 안에서 ScrollController, AnimationController, Future를 할당하면 매 프레임 다시 생성되고 이전 인스턴스가 누출됩니다. 이 객체들은 initStatedispose에서 해제되는 late final 필드에 속합니다.

무거운 동기 작업이 두 번째 함정입니다. build 안에서 JSON을 파싱하거나, 큰 리스트를 필터링하거나, 날짜를 포맷하면, 기저 데이터가 바뀌지 않았어도 그 작업이 매 다시 그리기마다 재실행됩니다. 입력이 바뀔 때 결과를 한 번만 계산해 캐시하거나, 프레임워크가 한 프레임을 막고 기다리지 않도록 FutureBuilder로 옮기십시오. 다른 값에서 파생되는 값은 의존성이 바뀔 때만 재계산하는 게터 뒤에 메모이제이션하십시오.

Opacity 위젯은 이름을 밝힐 만한 구체적인 원흉입니다. 하위 트리를 Opacity로 감싸면 화면 밖 saveLayer가 강제되는데, 이는 파이프라인에서 가장 비싼 연산 중 하나입니다. 페이드 애니메이션에는 AnimatedOpacityFadeTransition이 더 저렴하고, 고정된 색조에는 foregroundDecoration이나 셰이더를 통해 페인트 수준에서 색을 적용하면 레이어를 완전히 피할 수 있습니다. 같은 주의가 큰 표면에서 안티에일리어싱을 켠 ClipPathClipRRect에도 적용되며, 이들 역시 뒤에서 saveLayer를 유발합니다.

마지막으로, const 컬렉션과 매 빌드마다 새 클로저를 포획하지 않는 콜백을 선호하십시오. 기본값으로 전달한 const []와 인라인 람다 대신의 메서드 참조는 모두 위젯 동등성 검사를 저렴하게 유지하며, 이는 앞서 설명한 const 단락(短絡)이 실제로 발동하도록 해 줍니다.

긴 리스트와 이미지를 효율적으로 구성하기

긴 피드와 이미지 그리드는 순진한 Flutter 코드가 프레임을 떨어뜨리는 지점입니다. 두 가지 규칙이 대부분의 경우를 다룹니다. 행을 지연 구성하는 것, 그리고 이미지를 소스 해상도가 아니라 표시되는 크기로 디코딩하는 것입니다.

feed.dartdart
// Lazy rows plus right-sized image decoding keep scrolling at 60fps.

class Feed extends StatelessWidget {
  const Feed({required this.posts, super.key});
  final List<Post> posts;

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: posts.length,     // build on demand, not all at once
      cacheExtent: 600,            // pre-build past the viewport edge
      itemBuilder: (context, index) {
        final post = posts[index];
        return ListTile(
          leading: Image.network(
            post.thumbnailUrl,
            width: 48,
            height: 48,
            cacheWidth: 96,        // decode at display size, save memory
          ),
          title: Text(post.title),
        );
      },
    );
  }
}

3000픽셀 전체 해상도 이미지를 48픽셀 아바타에 욱여넣으면, 그 하나하나마다 메모리와 디코딩 시간을 낭비합니다. cacheWidth를 설정하면 엔진에 목표 크기로 디코딩하라고 지시하게 되며, 이것만으로도 리스트의 메모리 사용량을 한 자릿수 규모로 줄일 수 있습니다. Flutter의 성능 모범 사례는 같은 원칙을 따르는 Opacity, saveLayer, 클립 사용에 대한 추가 지침을 모아 둡니다.

2026년 Flutter 최적화를 DevTools로 측정하기

위의 모든 규칙은 가정이 아니라 검증되어야 합니다. Flutter DevTools가 진실의 원천입니다. 성능 오버레이(디버그 콘솔의 P 또는 flutter run --profile)는 UI와 래스터라는 두 그래프를 그리며, 초록 선을 넘는 막대는 모두 떨어진 프레임입니다. 이어서 타임라인 뷰가 각 느린 프레임을 특정 build, 레이아웃, 페인트 호출에 귀속시킵니다.

항상 실제 기기에서 --profile 모드로 프로파일하십시오. 디버그 빌드는 최적화되지 않은 Dart를 실행하고 모든 측정치를 부풀리므로, 디버그에서 버벅이는 화면이 컴파일 후에는 완벽하게 도는 경우가 많습니다. DevTools 성능 가이드는 프레임 차트를 읽고 버벅임의 원인을 찾아내는 과정을 짚어 주며, 같은 프로파일링 근육은 Flutter 애니메이션과 렌더링에 관한 면접에서도 등장합니다.

빠른 프로파일링 루프

프로파일 모드로 실행하고, 버벅이는 상호작용을 재현하고, 타임라인을 열어 프레임을 지속 시간으로 정렬하십시오. 최악의 프레임은 거의 언제나 지나치게 큰 단일 리빌드나 캐시되지 않은 이미지를 가리키며, 이는 추측하는 것보다 훨씬 빠릅니다.

더 넓은 Flutter 기술 허브는 프로덕션 성능 전략을 완성하는 상태 관리, 테스트, 렌더링 주제를 연결합니다.

결론

2026년 Flutter 성능은 미세한 요령보다는, 일관되게 적용하는 짧은 습관 목록에 관한 것입니다.

  • Impeller 위에서 배포하고 오래된 셰이더 워밍업 코드를 삭제하십시오. 첫 프레임 버벅임 문제는 엔진 수준에서 해결되었습니다.
  • 정적 위젯을 const로 표시하고 prefer_const_constructors 린트로 강제하십시오.
  • 상태를 가진 위젯을 작게 분할해, 각 setState가 가능한 한 좁은 하위 트리를 다시 그리게 하십시오.
  • 변하는 값을 화면 전체를 리빌드하는 대신 ValueListenableBuilderselect 스타일 API로 브로드캐스트하십시오.
  • 정말로 애니메이션되는 영역을 RepaintBoundary로 감싸되, 더 추가하기 전에 측정하십시오.
  • 리스트는 ListView.builder로 구성하고 이미지는 cacheWidth로 표시 크기에 맞춰 디코딩하십시오.
  • 실제 하드웨어에서 --profile 모드로 프로파일하고, 다음에 무엇을 최적화할지 직감이 아니라 DevTools가 결정하게 하십시오.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Flutter 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 7월 7일 업데이트

태그

#flutter
#performance
#impeller
#best-practices
#dart
#mobile

공유

관련 기사