effidevFlutter · Cloudflare 엣지 · 클라우드 비용 최적화
한국어

Flutter DevTools 프로파일링 실전: 메모리 누수(Memory Leak) 추적 및 CPU 프레임 드랍 잡기

Flutter DevTools Profiling and Performance Optimization

Flutter DevTools 프로파일링 실전: 메모리 누수 추적 및 CPU 프레임 드랍 잡기

Flutter 애플리케이션의 복잡도가 높아지면 리스트 스크롤 시 프레임 드랍(Jank)이 발생하거나, 화면을 전환할 때 메모리 사용량이 계속 증가하여 앱이 강제 종료(OOM)되는 문제를 겪게 됩니다.

이러한 성능 병목을 직관에 의존해 해결하려 하면 막대한 시간이 소요됩니다. Flutter DevTools는 메모리 힙 덤프 분석, CPU 프로파일러, Timeline 측정 등을 통해 버벅임과 메모리 누수의 원인을 정확히 짚어낼 수 있는 강력한 무기입니다.

이번 글에서는 Flutter DevTools를 활용하여 CPU 프레임 드랍 원인 분석메모리 누수(Memory Leak) 추적 기법을 실전 코드와 함께 다룹니다.


1. CPU 프레임 드랍(Jank) 추적과 Performance View

Flutter는 1초에 60회(또는 120회) 화면을 다시 그립니다. 16.6ms(또는 8.3ms) 이내에 프레임 조립 및 렌더링이 완료되지 않으면 Jank(프레임 드랍)가 발생합니다.

DevTools Timeline 분석 단계

  1. 앱을 profile 모드로 실행합니다:
    flutter run --profile
  2. DevTools를 열고 Performance 탭으로 이동합니다.
  3. 빨간색으로 표시된 타임라인 바(Budget 초과 프레임)를 클릭합니다.
  4. UI ThreadRaster Thread 중 어느 곳에서 지연이 발생했는지 확인합니다.
스레드 구분 지연 주요 원인 해결 방안
UI Thread build() 메서드 내 과도한 객체 생성을 비롯한 무거운 CPU 연산 Isolate.run() 분리, const 생성자 활용, Widget 재렌더링 범위 축소
Raster Thread 과도한 SaveLayer, 불필요한 Clip, 쉐이더(Shader) 복잡도 RepaintBoundary 추가, Opacity 대신 Color.withValues() 사용

실전 코드: UI 스레드 병목 해결

다음은 스크롤 시 UI 스레드를 점유하여 프레임 드랍을 유발하는 나쁜 예시와 개선 방식입니다.

// ❌ BAD: build() 및 스크롤 시 무거운 연산 실행 (UI Thread Blocking)
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      // 빌드 중에 복잡한 데이터 파싱 또는 정렬 수행
      final processedData = heavyComputation(items[index]); 
      return Text(processedData);
    },
  );
}

// ✅ GOOD: 연산을 Isolate로 이전 및 Compute 캐싱
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return FutureBuilder<String>(
        future: Isolate.run(() => heavyComputation(items[index])),
        builder: (context, snapshot) {
          if (!snapshot.hasData) return const CircularProgressIndicator();
          return Text(snapshot.data!);
        },
      );
    },
  );
}

2. Memory Leak(메모리 누수) 진단과 DevTools Memory View

메모리 누수는 더 이상 필요하지 않은 객체가 Garbage Collector(GC)에 의해 해제되지 않고 힙(Heap) 메모리에 남아있는 현상입니다.

Memory Leak 발생 패턴 3가지

  1. Unsubscribed Stream/AnimationController: State가 dispose되어도 구독이 살아있는 경우
  2. Global/Static reference: 싱글톤이나 전역 변수가 BuildContext나 State 객체를 계속 참조하는 경우
  3. Closure capturing: 비동기 콜백에서 해제되어야 할 State 객체를 캡처하는 경우

DevTools Memory Snapshot으로 추적하는 법

DevTools의 Memory 탭에서 Snapshot 기능을 활용하면 누수 객체를 직관적으로 찾아낼 수 있습니다.

  1. Snapshot 1 촬영: 특정 화면에 진입하기 전 메모리 상태 기록
  2. 화면 진입 및 이탈 반복: 해당 화면을 5~10회 열었다 닫음
  3. GC 실행: DevTools의 ‘GC’ 버튼을 클릭하여 정리되지 않는 객체 수집
  4. Snapshot 2 촬영: Snapshot 1과 Diff 비교 수행
  5. 해당 화면의 State 객체가 힙에 남아있는지 Retention Path 확인

3. 실전 메모리 누수 수정 패턴

패턴 1: StreamSubscription & AnimationController 누수

// ❌ BAD: dispose에서 스티림과 애니메이션 해제 누락
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _subscription = eventBus.stream.listen((event) {
      setState(() {});
    });
  }

  // dispose()가 구현되지 않아 메모리 누수 발생!
}

// ✅ GOOD: dispose() 구현으로 메모리 해제
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _subscription = eventBus.stream.listen((event) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _subscription.cancel();
    super.dispose();
  }
}

요약 및 결론

Flutter 앱의 최적화는 추측이 아닌 데이터와 측정을 바탕으로 이루어져야 합니다.

[!NOTE]

  1. 프레임 드랍 추적 시 반드시 flutter run --profile 모드로 진단하세요.
  2. UI 스레드 과부하는 Isolate.run()으로 분리하고, Raster 스레드는 RepaintBoundary와 그래픽 연산 단순화로 해결하세요.
  3. DevTools Memory Snapshot의 Diff 기능을 통해 화면 이탈 후에도 남아있는 State 객체의 Retention Path를 추적하여 해결하세요.

Flutter DevTools를 주기적 CI 렌더링 검사 및 개발 프로세스에 통합하면 60fps의 매끄러운 사용자 경험을 보장할 수 있습니다.