Tuesday, June 17, 2025

Flutter `const` 제대로 알고 쓰기: 성능 최적화의 첫걸음

Flutter 개발을 하다 보면 const 키워드를 마주치는 순간이 많습니다. 어떤 위젯 앞에는 붙어있고, 어떤 위젯 앞에는 없습니다. 안드로이드 스튜디오나 VS Code는 "이 위젯은 const로 만들 수 있어요!"라며 파란 줄을 긋기도 하죠. 많은 개발자들이 이 const를 단순히 '상수'를 의미하는 키워드로만 이해하고 넘어가거나, 린터(Linter)가 시키는 대로 기계적으로 추가하곤 합니다. 하지만 Flutter에서 const는 단순한 상수를 넘어, 앱의 성능을 극적으로 향상시킬 수 있는 매우 중요한 열쇠입니다.

이 글에서는 const가 왜 중요한지, final과는 무엇이 다른지, 그리고 const를 언제 어떻게 사용해야 앱의 퍼포먼스를 최대한으로 끌어올릴 수 있는지 구체적인 예시와 함께 깊이 있게 알아보겠습니다.

1. `const`와 `final`의 결정적 차이: 컴파일 타임 vs 런타임

const를 이해하기 위해선 final과의 차이점을 명확히 알아야 합니다. 둘 다 '한 번 할당되면 변경할 수 없는 변수'를 선언할 때 사용하지만, 값이 결정되는 시점이 완전히 다릅니다.

  • final (런타임 상수): 앱이 실행되는 동안(런타임) 값이 결정됩니다. 한 번 할당되면 바꿀 수 없지만, 그 값은 앱이 실행될 때 계산되거나 외부(API 등)로부터 받아올 수 있습니다.
  • const (컴파일 타임 상수): 코드가 컴파일되는 시점에 값이 결정되어야 합니다. 즉, 앱이 빌드될 때 이미 그 값이 무엇인지 명확하게 알고 있어야 합니다. 변수뿐만 아니라 객체(위젯 등)에도 사용할 수 있습니다.

예시를 통해 살펴보겠습니다.


// final: 앱 실행 시 현재 시간을 가져오므로 OK
final DateTime finalTime = DateTime.now();

// const: DateTime.now()는 실행 시점에 결정되므로 컴파일 에러 발생
// const DateTime constTime = DateTime.now(); // ERROR!

// const: 컴파일 시점에 값을 알 수 있으므로 OK
const String appName = 'My Awesome App';

이 차이점이 Flutter 위젯 트리에서 엄청난 성능 차이를 만들어냅니다.

2. `const`가 Flutter 성능을 향상시키는 두 가지 핵심 원리

const를 사용하는 것이 성능에 좋을까요? 이유는 크게 두 가지입니다: '메모리 재사용''불필요한 리빌드(Rebuild) 방지'입니다.

2.1. 메모리 효율성: 동일 객체 공유 (Canonical Instances)

const로 생성된 객체는 '정규 인스턴스(Canonical Instance)'가 됩니다. 이는 컴파일 시점에 값이 완전히 동일한 const 객체가 있다면, 앱 전체에서 단 하나의 인스턴스만 생성하고 모두가 그것을 공유한다는 의미입니다.

예를 들어, 앱의 여러 화면에서 동일한 간격을 주기 위해 const SizedBox(height: 20)를 100번 사용했다고 가정해 봅시다.


// const를 사용한 경우
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('첫 번째 아이템'),
      const SizedBox(height: 20), // A 인스턴스
      Text('두 번째 아이템'),
      const SizedBox(height: 20), // A 인스턴스를 재사용
      // ... 98번 더 반복
    ],
  );
}

이 경우, SizedBox(height: 20) 객체는 메모리에 단 하나만 생성되고, 100개의 모든 호출이 이 하나의 객체 주소를 참조합니다. 반면, const를 빼면 어떻게 될까요?


// const를 사용하지 않은 경우
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('첫 번째 아이템'),
      SizedBox(height: 20), // B 인스턴스 생성
      Text('두 번째 아이템'),
      SizedBox(height: 20), // C 인스턴스 생성 (B와 다름)
      // ... 98개의 새로운 인스턴스 생성
    ],
  );
}

const가 없으면 build 메소드가 호출될 때마다 새로운 SizedBox 객체가 100개 생성됩니다. 이는 불필요한 메모리 낭비와 가비지 컬렉터(GC)의 부담을 증가시켜 앱의 전반적인 성능 저하로 이어질 수 있습니다.

Dart의 identical() 함수를 사용하면 두 객체가 완전히 동일한 메모리 주소를 가리키는지 확인할 수 있습니다.


void checkIdentity() {
  const constBox1 = SizedBox(width: 10);
  const constBox2 = SizedBox(width: 10);
  print('const: ${identical(constBox1, constBox2)}'); // 출력: const: true

  final finalBox1 = SizedBox(width: 10);
  final finalBox2 = SizedBox(width: 10);
  print('final: ${identical(finalBox1, finalBox2)}'); // 출력: final: false
}

2.2. 렌더링 최적화: 불필요한 리빌드(Rebuild) 방지

이것이 const를 사용해야 하는 가장 중요한 이유입니다.

Flutter는 상태(State)가 변경될 때 setState()를 호출하여 위젯 트리를 다시 빌드(리빌드)합니다. 이때 Flutter 프레임워크는 이전 위젯 트리와 새로운 위젯 트리를 비교하여 변경된 부분만 화면에 다시 그립니다. 이 과정에서 위젯이 const로 선언되어 있다면, Flutter는 "이 위젯은 컴파일 타임 상수로, 절대 변하지 않는 위젯"이라는 사실을 알고 있습니다. 따라서 해당 위젯과 그 자식 위젯들에 대한 비교 작업을 완전히 건너뛰고, 리빌드 과정에서 제외시킵니다.

상태가 변경되는 카운터 앱을 예로 들어보겠습니다.

`const`를 사용하지 않은 나쁜 예


class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // 이 AppBar는 내용이 변하지 않음에도 불구하고 매번 리빌드됨
        title: Text('Bad Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // 이 부분도 변하지 않지만 매번 리빌드됨
            SizedBox(height: 50), 
            Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

위 코드에서 플로팅 버튼을 누를 때마다 _counter가 변경되고 setState()가 호출됩니다. 그러면 build 메소드 전체가 다시 실행됩니다. 이 과정에서 실제로 변경된 것은 Text('$_counter') 위젯뿐이지만, AppBar, SizedBox, Text('This is a static text.') 등 전혀 변경될 필요가 없는 위젯들까지 모두 새로 생성되고 비교 과정을 거치게 됩니다. 이는 매우 비효율적입니다.

`const`를 활용한 좋은 예


class CounterScreen extends StatefulWidget {
  // 위젯 자체도 const로 만들 수 있음
  const CounterScreen({Key? key}) : super(key: key);

  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // const 추가: 이 AppBar는 이제 리빌드 대상에서 제외됨
        title: const Text('Good Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 이 텍스트는 변하지 않으므로 const
            const Text('You have pushed the button this many times:'),
            // 이 텍스트는 _counter 값에 따라 변하므로 const가 아님
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // const 추가: 이 SizedBox는 리빌드 대상에서 제외됨
            const SizedBox(height: 50),
            // const 추가: 이 텍스트는 리빌드 대상에서 제외됨
            const Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        // const 추가: Icon도 리빌드 대상에서 제외됨
        child: const Icon(Icons.add),
      ),
    );
  }
}

이제 버튼을 누르면 build 메소드는 여전히 호출되지만, Flutter는 const로 표시된 위젯들(AppBar, Text, SizedBox, Icon)을 보고 "아, 이것들은 바뀔 리가 없으니 그냥 건너뛰자!"라고 판단합니다. 결과적으로 실제로 변경이 필요한 Text('$_counter') 위젯만 업데이트하게 되어 렌더링 성능이 크게 향상됩니다.

3. `const` 활용 전략: 언제, 어디에 사용해야 할까?

성능 향상을 위해 const를 적극적으로 사용하는 습관을 들이는 것이 좋습니다. 다음은 const를 적용할 수 있는 주요 위치입니다.

3.1. 위젯 생성자 (Widget Constructors)

가장 흔하고 효과적인 사용처입니다. Text, SizedBox, Padding, Icon 등 내용이 고정된 위젯을 생성할 때는 항상 const를 붙이는 습관을 가지세요.


// GOOD
const Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

// BAD
Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

EdgeInsets.all(16.0) 역시 const로 만들 수 있으므로, Padding 위젯 전체가 const가 될 수 있습니다.

3.2. 나만의 `const` 생성자 만들기

재사용 가능성이 높은 나만의 위젯을 만들 때, const 생성자를 제공하는 것은 매우 중요합니다. 위젯의 모든 final 멤버 변수가 컴파일 타임 상수가 될 수 있다면 const 생성자를 만들 수 있습니다.


class MyCustomButton extends StatelessWidget {
  final String text;
  final Color color;

  // 생성자를 const로 선언
  const MyCustomButton({
    Key? key,
    required this.text,
    this.color = Colors.blue,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ... 위젯 빌드 로직
    return Container(
      color: color,
      child: Text(text),
    );
  }
}

// 사용할 때
// 이제 이 위젯도 const로 생성하여 리빌드를 방지할 수 있다.
const MyCustomButton(text: 'Click Me')

3.3. 변수 및 컬렉션 (Variables and Collections)

앱 전역에서 사용되는 상수 값들, 예를 들어 색상, 패딩 값, 특정 문자열 등은 const 변수로 선언하여 관리하는 것이 좋습니다.


// lib/constants.dart
import 'package:flutter/material.dart';

const Color kPrimaryColor = Color(0xFF6F35A5);
const double kDefaultPadding = 16.0;

const List<String> kWelcomeMessages = [
  'Hello',
  'Welcome',
  'Bienvenido',
];

이렇게 선언된 상수들은 컴파일 시점에 값이 고정되며, 메모리 효율성도 높일 수 있습니다.

3.4. 린터 규칙(Linter Rules) 활용하기

const를 빠뜨리지 않고 사용하도록 강제하는 것은 좋은 습관입니다. 프로젝트 루트의 analysis_options.yaml 파일에 다음 규칙들을 추가하면 IDE가 const를 추가하라고 알려주거나 자동으로 수정해 줍니다.


linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_const_constructors_in_immutables
  • prefer_const_constructors: const로 만들 수 있는 생성자 호출에 const를 붙이도록 권장합니다.
  • prefer_const_declarations: const로 선언할 수 있는 최상위 변수나 정적 변수에 const를 사용하도록 권장합니다.
  • prefer_const_constructors_in_immutables: @immutable 클래스에 const 생성자를 추가하도록 권장합니다.

결론: `const`는 선택이 아닌 필수

Flutter에서 const는 단순히 '상수'를 의미하는 키워드가 아닙니다. 메모리를 절약하고, CPU의 불필요한 연산을 줄여 앱의 렌더링 성능을 최적화하는 가장 간단하면서도 강력한 도구입니다. 특히 복잡한 UI를 가진 앱이나 저사양 기기에서도 부드러운 사용자 경험을 제공하기 위해서는 const의 적극적인 활용이 필수적입니다.

이제부터 코드를 작성할 때, "이 위젯은 내용이 바뀌나?"라고 스스로에게 질문해 보세요. 만약 대답이 "아니오"라면, 주저하지 말고 const를 붙여주세요. 이 작은 습관 하나가 모여 당신의 Flutter 앱을 훨씬 더 빠르고 효율적으로 만들어 줄 것입니다.


0 개의 댓글:

Post a Comment