Flutter/Flutter

<번역/정리> 10 Lesser-Known Dart and Flutter Functionalities You Should Start Using - 2편

복복씨 2025. 6. 30. 13:30

 

https://medium.com/dcm-analyzer/10-lesser-known-dart-and-flutter-functionalities-you-should-start-using-6f931460ec71

 

10 Lesser-Known Dart and Flutter Functionalities You Should Start Using

Let’s uncover some of lesser-known functionalities that can simplify your development process and add a unique touch to your apps.

medium.com

 

*  이 글은 원문을 있는 그대로 번역하기 보단, 최대한 원문을 반영해 보다 나은 이해를 위해 정리하고, 쉽게 풀어쓴 글입니다

 

Expando


 

Dart의 Expando<T> 클래스는 기존 객체의 구조를 변경하지 않고도 추가 속성(데이터)을 동적으로 연결할 수 있도록 도와줍니다. 특히 다음과 같은 경우에 유용합니다:

  • 이미 정의된 클래스(서드파티 라이브러리 포함)를 변경할 수 없을 때
  • 불변 객체(immutable)에 데이터를 추가하고 싶을 때
  • HTML DOM 요소나 시스템 객체와 연결할 때

기본 예제

- 객체에 직접 속성을 추가하지 않고도, 외부 데이터를 연결하는 방법

import 'dart:html';

// 함수 타입 정의
typedef CustomFunction = void Function(int foo, String bar);

// DOM 요소에 연결할 수 있는 Expando 정의
Expando<CustomFunction> domFunctionExpando = Expando<CustomFunction>();

void main() {
  final myElement = DivElement();

  // Expando를 통해 함수 연결
  domFunctionExpando[myElement] = someFunc;

  // 연결된 함수 실행
  domFunctionExpando[myElement]?.call(42, 'expandos are cool');
}

void someFunc(int foo, String bar) {
  print('Hello. $foo $bar');
}

이 코드에서 일어나는 일

  • Expando<CustomFunction>는 객체를 키로 사용해 데이터를 숨겨서 저장합니다.
  • myElement라는 DOM 요소에 someFunc이라는 함수를 연결합니다.
  • 나중에 다시 해당 요소를 키로 사용해서 그 함수에 접근하고 실행합니다.

 

출력 결과

Hello. 42 expandos are cool

Expando의 내부 구조

class Expando<T extends Object> {
  final String? name;
  
  Expando([this.name]);

  // Retrieves the value associated with the object
  T? operator [](Object object) => _get(object);

  // Associates a value with an object
  void operator []=(Object object, T? value) => _set(object, value);

  T? _get(Object object) {
    // Internal lookup mechanism
  }

  void _set(Object object, T? value) {
    // Internal storage mechanism
  }
}

핵심 개념 정리

  • 약한 참조(Weak Reference): Expando는 객체가 더 이상 사용되지 않으면 연결된 값도 함께 제거됩니다 (메모리 누수 없음).
    - 어떤 객체를 참조하고 있지만, GC(Garbage Collector)가 해당 객체를 메모리에서 제거하는 것을 막지 않는 참조 방식입니다.
  • Object만 사용 가능: int, String, double 등의 기본형(primitive type)에는 Expando를 사용할 수 없습니다. 같은 숫자나 문자열이 재사용되는 경우가 많기 때문입니다.
  • 디버깅을 위한 이름 지정 가능: Expando('myExpando')처럼 이름을 부여하면, 디버깅 도구에서 식별하기 쉬워집니다.

실전 활용 예제

객체에 메타데이터 붙이기

Expando<int> objectIds = Expando<int>();
final user1 = User();
final user2 = User();

objectIds[user1] = 101;
objectIds[user2] = 202;

print(objectIds[user1]); // 101
print(objectIds[user2]); // 202

메모리 자동 정리

void test() {
  Expando<String> tempData = Expando<String>();
  var myObject = Object();

  tempData[myObject] = "Temporary Info";
  print(tempData[myObject]); // "Temporary Info"

  myObject = Object(); // 이전 객체는 더 이상 참조되지 않아 GC 대상
  print(tempData[myObject]); // null (새로운 객체)
}

기본형(primitive)에서는 사용 불가

Expando<String> expando = Expando<String>();

expando[42] = "Number"; // ❌ 에러: primitive type에는 사용 불가
expando["hello"] = "String"; // ❌ 에러

Flutter에서 위젯 별 고유 ID 저장

Expando<String> widgetKeys = Expando<String>();

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    widgetKeys[this] = "WidgetID_\${DateTime.now().millisecondsSinceEpoch}";
  }

  @override
  Widget build(BuildContext context) {
    return Text(widgetKeys[this] ?? "No ID");
  }
}

사용 추천 시나리오

  • 객체(특히 외부 라이브러리 또는 시스템 객체)에 추가 데이터를 붙이고 싶을 때
  • 객체를 확장(subclassing) 없이 추적하거나 식별할 때
  • 메모리 누수 없이 임시 데이터를 저장하고 싶을 때
  • Flutter 위젯에 별도 상태 정보를 부가적으로 부여하고 싶을 때

사용 자제 시나리오

  • 앱 재시작 시에도 데이터가 지속되어야 하는 경우
  • 객체의 정체성이 명확하지 않은 경우 (짧게 살아있는 객체 등)
  • 기본형 타입을 키로 사용하려는 경우 (int, String 등)

Expando는 객체 중심의 메타데이터 저장과 관리에 아주 유용하지만, 반드시 목적에 맞게 사용하는 것이 중요합니다.

 

 

addPostFrameCallback


 

Flutter에서 UI는 프레임 단위로 렌더링됩니다. 이 과정에서 어떤 작업은 하나의 프레임이 끝난 직후에 실행되어야 할 때가 있는데, 그럴 때 사용하는 메서드가 바로 addPostFrameCallback입니다.

언제 사용하나요?

예를 들어 위젯의 크기나 위치 정보를 얻고 싶을 때가 있습니다. 하지만 initState()는 위젯이 아직 완전히 렌더링되기 전 단계에서 호출되기 때문에, 이 시점에서 context.findRenderObject()를 호출하면 null이 반환되거나 정확하지 않은 값이 나올 수 있습니다. 이럴 때 addPostFrameCallback()을 사용하면 UI가 렌더링된 이후 시점에 안전하게 작업을 수행할 수 있습니다.

@override
void initState() {
  super.initState();

  WidgetsBinding.instance.addPostFrameCallback((_) {
    final box = context.findRenderObject() as RenderBox;
    print('Widget size: ${box.size}');
  });
}

핵심 요약

  • addPostFrameCallback()은 현재 프레임이 끝난 직후에 콜백을 실행합니다.
  • initState()에서 위젯의 크기를 직접 확인할 수 없을 때 유용합니다.
  • Future.delayed(Duration.zero, () {}) 방식보다 안정적입니다.
  • 단, 한 번만 실행됩니다. 반복적으로 실행하려면 addPersistentFrameCallback()을 사용해야 합니다.

예제 코드 분석

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() {
  runApp(MaterialApp(home: MyWidget()));
}

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final GlobalKey _key = GlobalKey();

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      final RenderBox box = _key.currentContext!.findRenderObject() as RenderBox;
      final size = box.size;
      print('Widget size: $size');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          key: _key,
          width: 200,
          height: 100,
          color: Colors.blue,
          child: const Center(child: Text("Hello")),
        ),
      ),
    );
  }
}

이 예제의 핵심은 initState() 시점에서는 위젯이 아직 완전히 렌더링되지 않았다는 점입니다. 따라서 위젯의 크기나 위치 정보를 정확하게 얻기 위해서는 addPostFrameCallback을 통해 렌더링이 완료된 이후에 작업을 수행해야 합니다.

 

일부 개발자들은 build() 이후에 코드를 실행하기 위해
Future.delayed(Duration.zero, () { ... }) 같은 꼼수(hack) 를 사용합니다.
하지만 이런 방식보다는 addPostFrameCallback을 사용하는 것이 올바른 방법입니다.

즉, 프레임이 완전히 그려진 직후 실행되길 원한다면, addPostFrameCallback을 써야 합니다.
Future.delayed(Duration.zero)는 의도치 않은 타이밍 문제를 일으킬 수 있기 때문입니다.


내부 동작 원리

이 메서드가 Flutter 내부에서 어떻게 구현되어 있는지를 들여다보는 것은 늘 흥미롭습니다.
Flutter 내부에서 addPostFrameCallback은 다음과 같이 구현되어 있습니다:

void addPostFrameCallback(FrameCallback callback, {String debugLabel = 'callback'}) {
  assert(() {
    if (debugTracePostFrameCallbacks) {
      final FrameCallback originalCallback = callback;
      callback = (Duration timeStamp) {
        Timeline.startSync(debugLabel);
        try {
          originalCallback(timeStamp);
        } finally {
          Timeline.finishSync();
        }
      };
    }
    return true;
  }());
  _postFrameCallbacks.add(callback);
}

이 함수는 프레임이 끝난 후 실행될 콜백(callback) 을 등록하는 함수입니다.
하지만 단순히 리스트에 콜백을 넣는 것 이상을 합니다.

1. assert(() { ... }())

  • 이 블록은 디버그 모드일 때만 실행됩니다. (assert는 릴리즈 모드에서는 실행되지 않음)

2. debugTracePostFrameCallbacks가 true이면?

  • 성능 분석용 로깅(Timeline tracing) 을 위해 콜백을 감쌉니다.
  • 즉, 콜백이 실행될 때 "언제 시작했고, 언제 끝났는지" 를 추적할 수 있게 해줍니다.
  • 내부적으로 Timeline.startSync()와 Timeline.finishSync()를 사용하여 성능 타임라인에 기록합니다.
callback = (Duration timeStamp) {
  Timeline.startSync(debugLabel);
  try {
    originalCallback(timeStamp);
  } finally {
    Timeline.finishSync();
  }
};

3. _postFrameCallbacks.add(callback);

  • 마지막 줄에서 콜백을 _postFrameCallbacks 리스트에 추가합니다.
  • 이 리스트에 있는 콜백들은 현재 프레임이 끝난 직후 한 번 실행됩니다.

주요 동작 요약

  • 콜백이 _postFrameCallbacks 리스트에 추가됩니다.
  • 현재 프레임이 완료된 후, 등록된 콜백이 순차적으로 실행됩니다.
  • 디버깅 옵션이 활성화된 경우, Timeline.startSync()와 Timeline.finishSync()를 통해 DevTools에서 성능 분석도 가능합니다.
  • 콜백은 단 한 번만 실행됩니다.

실전 활용 예시

위젯 속성 접근 (예: 크기, 위치)

WidgetsBinding.instance.addPostFrameCallback((_) {
  final box = context.findRenderObject() as RenderBox;
  print('Widget size: ${box.size}');
});

첫 렌더링 후 다이얼로그 표시

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    showDialog(context: context, builder: (_) => AlertDialog(title: Text("Welcome!")));
  });
}

 

이 코드는 위젯이 완전히 화면에 그려진 후에 해당 위젯의 크기를 안전하게 얻기 위해 사용하는 패턴입니다.

initState()는 위젯이 화면에 그려지기 전에 실행됩니다.
이 시점에는 아직 레이아웃 정보(크기, 위치 등)가 계산되지 않았기 때문에,
context.findRenderObject()로 위젯 정보를 바로 가져오면 null이거나, 잘못된 값이 나올 수 있습니다.

하지만 addPostFrameCallback()을 사용하면,
"화면이 다 그려지고 난 다음" 에 콜백이 실행되기 때문에
그 시점에서는 위젯의 크기 정보 등을 안전하게 가져올 수 있습니다.

 

빌드 완료 후 스크롤 이동

final ScrollController controller = ScrollController();

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    controller.jumpTo(100); // Scrolls smoothly to position 100 after build
  });
}

controller.jumpTo() 같은 스크롤 조작도 마찬가지입니다.
위젯이 아직 그려지지 않은 상태(initState) 에서는
ScrollController가 해당 뷰에 아직 연결되지 않았기 때문에
jumpTo()를 호출하면 동작하지 않거나, 에러가 날 수 있습니다.

그래서 꼭 addPostFrameCallback()을 써서 "레이아웃 완료 이후" 에 호출해야 제대로 작동합니다.

 

디버깅 시 이름 지정

WidgetsBinding.instance.addPostFrameCallback((_) {
  print('Callback executed!');
}, debugLabel: 'MyPostFrameCallback');

 

debugLabel은 디버깅할 때 도움이 되는 이름표입니다.
여러 개의 addPostFrameCallback()을 등록했을 때,
DevTools에서 이 이름을 통해 어떤 콜백이 얼마나 오래 걸렸는지 추적할 수 있습니다.

즉, 성능 이슈가 생겼을 때 원인을 빠르게 찾는 데 도움이 됩니다.


적절한 사용 시점

  • 위젯이 완전히 렌더링된 이후에 어떤 작업을 해야 할 때
  • 첫 프레임 이후 다이얼로그 표시, 애니메이션 실행, 사이즈 측정 등
  • 스크롤 위치를 자동으로 조정해야 할 때

피해야 할 사용 시점

  • 반복적으로 콜백을 실행해야 할 경우 → addPersistentFrameCallback() 사용
  • 무거운 연산이 필요한 경우 → 별도의 Isolate에서 처리
  • 새 프레임을 강제로 시작해야 하는 경우 → scheduleFrameCallback() 사용

 

결론


 

이번 글에서는 평소 잘 알려지지 않았지만, 매우 유용한 Dart와 Flutter의 기능들을 살펴보았습니다. 이러한 기능들은 앱의 기능을 더 풍부하게 만들어줄 뿐만 아니라, 개발 과정도 훨씬 단순하고 효율적으로 만들어줍니다.

또한, 단순히 기능만 사용하는 데서 그치는 것이 아니라, 이 기능들이 내부적으로 어떻게 동작하는지까지 살펴보면서 Dart와 Flutter의 구조를 더 깊이 이해할 수 있었던 시간이었다고 생각합니다.

 

1️⃣ Future.any 여러 Future 중 가장 먼저 끝나는 것 하나만 기다릴 때 사용합니다. 동시에 여러 작업을 시키고, 가장 빨리 끝난 결과만 가져오고 싶을 때 유용합니다.
2️⃣ scheduleMicrotask Future보다 더 빨리, 이벤트 루프의 마이크로태스크 큐에 넣어 코드를 실행시킵니다. 순서를 정밀하게 조정할 때 사용합니다.
3️⃣ compute Flutter에서 무거운 연산을 별도의 isolate로 분리해 UI 렉 없이 처리하는 함수입니다. jsonDecode나 이미지 처리 등에 적합합니다.
4️⃣ runZonedGuarded 앱 전체에 예외 처리 “울타리”를 만드는 함수입니다. 전역 예외 처리로그 수집에 사용됩니다.
5️⃣ Timeline API (생략) Flutter 성능 분석 도구에서 이벤트 타임라인을 커스텀 태그로 추적할 수 있게 도와줍니다. 성능 측정 시 유용합니다.
6️⃣ unawaited Future를 기다리지 않고 실행만 할 때 사용합니다. 무시해도 되는 비동기 작업을 명시적으로 표시하는 데 도움됩니다.
7️⃣ FutureRecord2 Dart 3부터 생긴 문법으로, 두 개의 비동기 값을 동시에 기다리고 구조 분해 할당하는 데 사용됩니다.
8️⃣ Expando 객체에 동적으로 속성을 붙일 수 있는 숨겨진 Map 같은 역할을 합니다. 서브클래싱 없이 메타데이터를 붙일 때 유용합니다. 약한 참조 기반으로 메모리 누수 없이 작동합니다.
9️⃣ addPostFrameCallback Flutter 위젯이 화면에 완전히 그려진 이후 실행할 콜백을 등록할 때 사용합니다. 위젯의 크기 측정, 자동 스크롤, 팝업 띄우기 등에서 활용됩니다.
🔟 addPersistentFrameCallback 매 프레임마다 반복적으로 실행되는 콜백을 등록합니다. 애니메이션 루프나 상태 업데이트에 사용됩니다. (addPostFrameCallback과는 다름)