성능 최적화 — DevTools, 리빌드 추적, const 생성자

Flutter는 기본적으로 성능이 좋지만, 앱이 커지면서 프레임 드롭이나 버벅임이 발생할 수 있습니다. 성능 문제를 찾고 해결하는 방법을 정리해보겠습니다.


Flutter DevTools

DevTools는 Flutter 공식 성능 분석 도구입니다.

BASH
# DevTools 실행 (앱이 실행 중일 때)
flutter run
# 터미널에 표시된 DevTools URL 접속

# 또는 직접 실행
dart devtools

주요 탭

용도
Flutter Inspector위젯 트리 탐색
Performance프레임별 성능 분석
CPU Profiler함수별 CPU 사용량
Memory메모리 사용량, 누수 탐지
Network네트워크 요청 모니터링

불필요한 리빌드 방지

1. const 생성자 사용

DART
// 나쁜 예: 매 빌드마다 새 인스턴스 생성
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('제목'),              // 매번 새로 생성
      SizedBox(height: 16),     // 매번 새로 생성
      Icon(Icons.star),         // 매번 새로 생성
    ],
  );
}

// 좋은 예: const로 재사용
Widget build(BuildContext context) {
  return Column(
    children: [
      const Text('제목'),       // 재사용
      const SizedBox(height: 16),
      const Icon(Icons.star),
    ],
  );
}

2. 위젯 분리

DART
// 나쁜 예: 전체 위젯이 리빌드
class MyScreen extends StatefulWidget {
  @override
  State<MyScreen> createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // count가 바뀌면 이것도 리빌드됨
        const HeavyWidget(),     // 불필요한 리빌드!
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('증가'),
        ),
      ],
    );
  }
}

// 좋은 예: 변하는 부분만 별도 위젯으로 분리
class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const HeavyWidget(),     // 리빌드 안 됨
        const CounterWidget(),   // 이것만 리빌드
      ],
    );
  }
}

3. ListView.builder 사용

DART
// 나쁜 예: 모든 아이템을 한 번에 생성
ListView(
  children: items.map((item) => ListTile(title: Text(item))).toList(),
)

// 좋은 예: 보이는 것만 생성
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)

4. RepaintBoundary

자주 변하는 위젯을 격리하여 다른 위젯의 리페인트를 방지합니다.

DART
RepaintBoundary(
  child: AnimatedWidget(),  // 이 위젯의 페인트가 부모에 영향 안 줌
)

빌드 메서드 최적화

DART
// 나쁜 예: build에서 무거운 작업
@override
Widget build(BuildContext context) {
  final processed = heavyProcessing(data);  // 매 빌드마다 실행!
  return Text(processed);
}

// 좋은 예: 상태 변경 시에만 처리
class _MyState extends State<MyWidget> {
  late String _processed;

  @override
  void initState() {
    super.initState();
    _processed = heavyProcessing(data);
  }

  @override
  Widget build(BuildContext context) {
    return Text(_processed);  // 캐싱된 값 사용
  }
}

이미지 최적화

DART
// 1. 적절한 크기로 리사이즈
Image.network(
  imageUrl,
  cacheWidth: 200,    // 디코딩 크기 제한
  cacheHeight: 200,
)

// 2. CachedNetworkImage 사용
CachedNetworkImage(
  imageUrl: url,
  memCacheWidth: 200,
)

// 3. precacheImage로 미리 로드
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  precacheImage(const AssetImage('assets/logo.png'), context);
}

애니메이션 성능

DART
// 나쁜 예: setState로 애니메이션
_controller.addListener(() {
  setState(() {});  // 전체 build 재실행
});

// 좋은 예: AnimatedBuilder 사용
AnimatedBuilder(
  animation: _controller,
  child: const MyStaticChild(),  // child는 리빌드 안 됨
  builder: (context, child) {
    return Transform.rotate(
      angle: _controller.value * 2 * pi,
      child: child,  // 재사용
    );
  },
)

프로파일 모드에서 테스트

BASH
# 프로파일 모드로 실행 (릴리스에 가까운 성능)
flutter run --profile

# 릴리스 모드
flutter run --release

디버그 모드는 JIT 컴파일, 어설션 체크 등으로 느립니다. 성능 테스트는 반드시 프로파일 모드에서 하세요.

면접 포인트: "디버그 모드에서 느린데요?"라는 질문에 "프로파일 모드에서 테스트해야 합니다. 디버그 모드는 JIT, 어설션, DevTools 오버헤드가 포함되어 실제 성능과 다릅니다"라고 답하세요.


체크리스트

PLAINTEXT
□ const 생성자를 최대한 활용했는가?
□ ListView.builder를 사용했는가?
□ 무거운 위젯을 분리했는가?
□ build에서 무거운 연산을 하고 있지 않은가?
□ 이미지 크기를 제한했는가?
□ 애니메이션에 AnimatedBuilder를 사용했는가?
□ 프로파일 모드에서 테스트했는가?
□ RepaintBoundary를 적절히 사용했는가?

정리

  • const 생성자로 위젯 재생성을 방지하세요 (가장 쉽고 효과적)
  • 변하는 부분과 변하지 않는 부분을 별도 위젯으로 분리하세요
  • ListView.builder()로 보이는 아이템만 생성하세요
  • AnimatedBuilderchild 파라미터로 정적 부분을 캐싱하세요
  • 성능 테스트는 반드시 프로파일 모드 에서 하세요
  • Flutter DevTools의 Performance 탭으로 프레임 드롭을 분석하세요
댓글 로딩 중...