최신 모바일 애플리케이션의 핵심은 데이터입니다. 사용자가 앱을 실행하는 순간부터, 앱은 네트워크를 통해 서버와 통신하고, 디바이스의 데이터베이스에서 정보를 읽어오며, 복잡한 연산을 수행합니다. 이러한 작업들은 대부분 즉시 완료되지 않는 '비동기(Asynchronous)' 작업입니다. Flutter는 이러한 비동기 데이터를 선언적 UI 프레임워크의 패러다임에 맞춰 우아하게 처리할 수 있는 강력한 도구, FutureBuilder와 StreamBuilder를 제공합니다. 이 두 위젯은 Flutter 개발자라면 반드시 숙달해야 할 필수 요소입니다.
이 글에서는 단순히 FutureBuilder와 StreamBuilder의 사용법을 나열하는 것을 넘어, 그들의 내부 동작 원리, 핵심적인 차이점, 그리고 실무에서 마주할 수 있는 흔한 함정과 해결책까지 깊이 있게 탐구합니다. Dart의 비동기 개념인 Future와 Stream의 본질부터 시작하여, 각 위젯의 생명주기, 상태 관리, 그리고 실제 애플리케이션 시나리오에 적용하는 고급 활용법까지 체계적으로 다룰 것입니다.
Dart의 비동기 핵심: Future와 Stream의 본질
FutureBuilder와 StreamBuilder를 제대로 이해하기 위해서는 먼저 그 기반이 되는 Dart의 비동기 개념, Future와 Stream에 대한 명확한 이해가 선행되어야 합니다.
Future: 언젠가 완료될 하나의 값
Future는 이름 그대로 '미래'의 어떤 시점에 완료될 하나의 결과값을 나타내는 객체입니다. 이 결과값은 성공적인 데이터일 수도 있고, 작업 실패로 인한 에러일 수도 있습니다. Future는 비동기 작업이 시작되었음을 즉시 알리고, 작업이 완료되면 그 결과를 전달하는 '약속'과 같습니다.
- 네트워크 요청: HTTP API를 호출하여 사용자 정보를 가져오는 작업.
- 파일 I/O: 디바이스 저장소에서 큰 파일을 읽어오는 작업.
- 데이터베이스 쿼리: 로컬 데이터베이스에서 특정 데이터를 조회하는 작업.
이러한 작업들은 완료까지 시간이 걸리므로, Future는 앱이 멈추지 않고(UI가 프리징되지 않고) 다른 작업을 계속 수행할 수 있도록 해줍니다.
Dart에서는 async와 await 키워드를 사용하여 Future를 보다 쉽게 다룰 수 있습니다.
// 2초 후에 사용자 이름을 반환하는 가상 함수
Future<String> fetchUserName() async {
// 실제 앱에서는 이곳에 http.get()과 같은 네트워크 요청 코드가 들어갑니다.
await Future.delayed(const Duration(seconds: 2));
return 'John Doe';
}
void main() async {
print('Fetching user name...');
String userName = await fetchUserName(); // 2초간 기다린 후 결과를 userName에 할당
print('User name: $userName'); // 'User name: John Doe' 출력
}
Stream: 시간의 흐름에 따른 데이터의 연속
Future가 단 한 번의 결과만을 약속한다면, Stream은 시간의 흐름에 따라 여러 개의 데이터 이벤트(또는 에러)를 순차적으로 전달하는 통로입니다. 스트림은 마치 컨베이어 벨트와 같아서, 데이터가 생길 때마다 벨트 위로 흘려보내고, 구독자는 이 데이터를 지속적으로 받아 처리할 수 있습니다.
- 실시간 채팅: 새로운 메시지가 도착할 때마다 화면에 표시.
- 파일 다운로드 진행률: 다운로드 상태가 변경될 때마다(예: 10%, 20%...) UI 업데이트.
- 사용자 입력: 텍스트 필드에 입력되는 값의 변화를 실시간으로 감지.
- 센서 데이터: GPS 위치 정보나 자이로스코프 센서 값의 지속적인 변화.
Stream은 두 가지 종류가 있습니다:
- 단일 구독(Single-subscription) 스트림: 오직 하나의 리스너(구독자)만 가질 수 있으며, 데이터 전체를 순서대로 전달하는 것이 중요할 때 사용됩니다. 파일 읽기 등이 예시입니다.
- 브로드캐스트(Broadcast) 스트림: 여러 리스너가 동시에 구독할 수 있으며, 언제든 구독을 시작하고 취소할 수 있습니다. 마우스 이벤트나 버튼 클릭과 같은 일반적인 이벤트 처리에 적합합니다.
// 1초마다 숫자를 1씩 증가시켜 내보내는 스트림 생성
Stream<int> countStream() async* {
int i = 0;
while (true) {
await Future.delayed(const Duration(seconds: 1));
yield i++; // yield 키워드를 사용하여 스트림에 데이터를 추가
}
}
void main() {
Stream<int> stream = countStream();
// 스트림을 구독(listen)하여 데이터가 올 때마다 콜백 함수 실행
final subscription = stream.listen((data) {
print('Received: $data');
});
// 5초 후에 구독을 취소하여 스트림 수신 중단
Future.delayed(const Duration(seconds: 5), () {
subscription.cancel();
});
}
FutureBuilder: 일회성 비동기 연산의 UI 표현
FutureBuilder는 Future의 상태 변화에 따라 UI를 동적으로 빌드하는 위젯입니다. Future가 아직 실행 중일 때는 로딩 인디케이터를, 성공적으로 완료되면 결과 데이터를, 실패하면 에러 메시지를 보여주는 등의 로직을 손쉽게 구현할 수 있습니다.
FutureBuilder의 구조와 작동 원리
FutureBuilder의 핵심 생성자는 다음과 같습니다.
FutureBuilder<T>({
Key? key,
Future<T>? future,
T? initialData,
required AsyncWidgetBuilder<T> builder,
})
future: 위젯이 관찰할Future객체입니다.initialData:Future가 완료되기 전에 사용할 초기 데이터입니다. 이를 설정하면 첫 프레임에서 로딩 상태 대신 초기 데이터를 보여줄 수 있어 사용자 경험을 향상시킬 수 있습니다.builder:Future의 상태가 변경될 때마다 호출되는 함수로, UI를 빌드하는 역할을 합니다. 이 함수는BuildContext와AsyncSnapshot을 인자로 받습니다.
생명주기의 핵심, AsyncSnapshot과 ConnectionState
builder 함수에 전달되는 AsyncSnapshot 객체는 Future의 현재 상태에 대한 모든 정보를 담고 있습니다. 이 객체를 통해 우리는 비동기 작업의 진행 상황을 파악하고 그에 맞는 UI를 구성할 수 있습니다.
AsyncSnapshot<T>의 주요 속성:
connectionState: 비동기 작업과의 연결 상태를 나타내는ConnectionState열거형 값입니다.data: 비동기 작업이 성공적으로 완료되었을 때의 결과 데이터입니다. 데이터가 아직 없거나 에러가 발생하면null일 수 있습니다.error: 비동기 작업에서 에러가 발생했을 때의 에러 객체입니다.stackTrace: 에러 발생 시의 스택 트레이스 정보입니다. 디버깅에 유용합니다.hasData:data가null이 아닌지 여부를 반환합니다.hasError:error가null이 아닌지 여부를 반환합니다.
ConnectionState의 상태들:
ConnectionState.none:future가null이거나 아직 연결되지 않은 초기 상태입니다.ConnectionState.waiting: 비동기 작업이 활성화되어 결과를 기다리는 중인 상태입니다. 이 상태에서 보통 로딩 인디케이터(CircularProgressIndicator)를 보여줍니다.ConnectionState.active:Stream에서 사용되는 상태로, 데이터가 지속적으로 들어오고 있는 활성 상태를 의미합니다.FutureBuilder에서는 거의 사용되지 않습니다.ConnectionState.done: 비동기 작업이 완료된 상태입니다. 성공(데이터가 있음) 또는 실패(에러가 있음) 두 가지 경우 모두 이 상태가 됩니다. 따라서snapshot.hasError를 추가로 확인하여 성공과 실패를 구분해야 합니다.
FutureBuilder<String>(
future: fetchUserName(), // 위에서 정의한 Future 함수
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// 1. ConnectionState를 확인하여 로직 분기
if (snapshot.connectionState == ConnectionState.waiting) {
// 로딩 중일 때
return const Center(child: CircularProgressIndicator());
} else if (snapshot.connectionState == ConnectionState.done) {
// 작업이 완료되었을 때
// 2. 에러 유무 확인
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
// 3. 데이터 유무 확인 및 UI 표시
if (snapshot.hasData) {
return Center(child: Text('Welcome, ${snapshot.data}'));
} else {
return const Center(child: Text('No data found.'));
}
} else {
// ConnectionState.none 또는 기타 상태
return const Center(child: Text('Press a button to start.'));
}
},
)
흔히 발생하는 함정: 불필요한 재실행 문제
FutureBuilder를 처음 사용할 때 가장 흔하게 저지르는 실수는 future 속성에 Future를 반환하는 함수를 직접 호출하는 것입니다.
// 👎 잘못된 사용 예시
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: fetchUserName(), // 🚨 문제의 코드!
builder: (context, snapshot) {
// ... builder 로직 ...
},
);
}
}
위 코드는 왜 문제일까요? Flutter에서 위젯의 build 메소드는 부모 위젯이 리빌드되거나, 화면 회전, 테마 변경 등 다양한 이유로 여러 번 호출될 수 있습니다. 위와 같이 코드를 작성하면 build 메소드가 호출될 때마다 fetchUserName() 함수가 새롭게 호출되어 새로운 Future 객체가 생성됩니다. 이는 불필요한 네트워크 요청을 반복하게 만들어 서버에 부하를 주고, 사용자에게는 로딩 인디케이터가 계속 깜빡이는 나쁜 경험을 제공합니다.
올바른 Future 제공 방법
이 문제를 해결하려면 Future 객체를 build 메소드 외부에서 단 한 번만 생성하고, 리빌드가 발생하더라도 동일한 Future 인스턴스를 참조하도록 해야 합니다. 가장 일반적인 방법은 StatefulWidget을 사용하는 것입니다.
// 👍 올바른 사용 예시
class UserProfileWidget extends StatefulWidget {
const UserProfileWidget({super.key});
@override
State<UserProfileWidget> createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State<UserProfileWidget> {
// 1. Future 객체를 상태 변수로 선언합니다.
late final Future<String> _userNameFuture;
// 2. initState에서 딱 한 번만 Future를 생성하고 할당합니다.
@override
void initState() {
super.initState();
_userNameFuture = fetchUserName();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User Profile')),
body: FutureBuilder<String>(
// 3. build 메소드에서는 상태 변수에 저장된 Future를 참조합니다.
future: _userNameFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
return Center(child: Text('Welcome, ${snapshot.data}', style: Theme.of(context).textTheme.headlineMedium));
}
// 기본적으로 로딩 상태를 보여줍니다.
return const Center(child: CircularProgressIndicator());
},
),
);
}
}
이 방법을 통해 UserProfileWidget이 여러 번 리빌드되더라도 _userNameFuture는 initState에서 생성된 최초의 인스턴스를 계속 유지하므로, fetchUserName() 함수는 불필요하게 재실행되지 않습니다. 상태 관리 라이브러리(Provider, Riverpod, BLoC 등)를 사용하면 이러한 상태를 위젯의 생명주기에서 분리하여 더욱 깔끔하게 관리할 수도 있습니다.
StreamBuilder: 연속적인 데이터 흐름의 실시간 UI 반영
StreamBuilder는 Stream으로부터 연속적으로 방출되는 데이터 이벤트를 수신하여 UI를 업데이트하는 위젯입니다. 실시간 채팅, 주식 시세, 알림 피드 등 지속적인 데이터 변화를 화면에 즉각적으로 반영해야 할 때 이상적인 해결책입니다.
StreamBuilder의 구조와 FutureBuilder와의 차이점
StreamBuilder의 생성자는 FutureBuilder와 매우 유사하며, future 대신 stream 속성을 사용합니다.
StreamBuilder<T>({
Key? key,
T? initialData,
Stream<T>? stream,
required AsyncWidgetBuilder<T> builder,
})
stream: 위젯이 구독할Stream객체입니다.initialData:Stream에서 첫 데이터가 도착하기 전에 사용할 초기 데이터입니다. 스트림은 첫 데이터를 받기까지 시간이 걸릴 수 있으므로,initialData를 제공하면 초기 로딩 상태 없이 UI를 즉시 구성할 수 있어 유용합니다.builder:Stream에서 새로운 이벤트(데이터 또는 에러)가 발생할 때마다 호출되어 UI를 리빌드합니다.
ConnectionState.active의 역할
StreamBuilder와 FutureBuilder의 가장 큰 차이점 중 하나는 ConnectionState를 다루는 방식입니다. FutureBuilder는 작업이 완료되면 ConnectionState.done으로 전환되고 더 이상 변하지 않습니다. 반면, StreamBuilder는 스트림이 열려 있고 계속해서 데이터를 방출할 수 있는 동안 ConnectionState.active 상태를 유지합니다. 새로운 데이터가 도착할 때마다 builder는 ConnectionState.active 상태의 새로운 AsyncSnapshot을 가지고 호출됩니다. 스트림이 닫혔을 때(더 이상 데이터를 방출하지 않을 때) 비로소 ConnectionState.done 상태가 됩니다.
스트림 관리와 리소스 해제
Future는 한 번 완료되면 끝나지만, Stream은 명시적으로 닫아주지 않으면 계속해서 리소스를 차지할 수 있습니다. 특히 직접 StreamController를 생성하여 스트림을 관리하는 경우, 위젯이 화면에서 사라질 때(dispose될 때) 스트림을 닫아주지 않으면 메모리 누수(memory leak)가 발생할 수 있습니다.
따라서 StatefulWidget의 dispose 메소드에서 스트림 컨트롤러를 닫아주는 것이 매우 중요합니다.
class ClockWidget extends StatefulWidget {
const ClockWidget({super.key});
@override
State<ClockWidget> createState() => _ClockWidgetState();
}
class _ClockWidgetState extends State<ClockWidget> {
// 직접 스트림을 생성하고 제어하기 위해 StreamController를 사용합니다.
final StreamController<DateTime> _clockController = StreamController<DateTime>();
late final Timer _timer;
@override
void initState() {
super.initState();
// 1초마다 현재 시간을 스트림에 추가하는 타이머를 설정합니다.
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_clockController.add(DateTime.now());
});
}
// 🚨 위젯이 제거될 때 리소스를 반드시 해제해야 합니다.
@override
void dispose() {
_timer.cancel(); // 타이머 중지
_clockController.close(); // 스트림 컨트롤러 닫기
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<DateTime>(
stream: _clockController.stream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active && snapshot.hasData) {
// 스트림이 활성 상태이고 데이터가 있을 때 시간을 표시
final dateTime = snapshot.data!;
return Center(
child: Text(
'${dateTime.hour}:${dateTime.minute}:${dateTime.second}',
style: Theme.of(context).textTheme.headlineLarge,
),
);
}
// 초기 상태 또는 데이터가 없을 때
return const Center(child: Text('Loading clock...'));
},
);
}
}
참고로, Firebase의 Firestore와 같은 외부 라이브러리가 제공하는 스트림은 대부분 라이브러리 내부적으로 생명주기를 관리해주므로, StreamBuilder가 위젯 트리에서 제거될 때 자동으로 구독이 취소되어 메모리 누수 걱정이 덜합니다. 하지만 직접 StreamController를 사용할 때는 dispose에서의 처리가 필수적입니다.
심층 분석: FutureBuilder vs. StreamBuilder
두 위젯의 차이점을 명확히 이해하고 적절한 상황에 사용하는 것이 중요합니다. 다음 표는 두 위젯의 핵심적인 차이를 요약한 것입니다.
| 특성 | FutureBuilder | StreamBuilder |
|---|---|---|
| 데이터 소스 | Future<T> |
Stream<T> |
| 결과 값의 수 | 단일 값 또는 에러 (한 번) | 여러 값 또는 에러 (지속적) |
| 주요 ConnectionState | .waiting → .done |
.waiting → .active (데이터 수신 중) → .done (스트림 종료) |
| 주요 사용 사례 | - REST API 호출 - 설정 파일 읽기 - 데이터베이스 1회성 쿼리 - 복잡한 계산 수행 |
- 웹소켓/실시간 데이터베이스 연동 (Firebase) - 파일 다운로드/업로드 진행률 표시 - 사용자 입력 감지 - 주기적인 데이터 폴링 |
고급 활용 및 최적화 전략
기본적인 사용법을 넘어, FutureBuilder와 StreamBuilder를 더 효과적으로 사용하기 위한 몇 가지 고급 전략이 있습니다.
정교한 에러 처리
단순히 snapshot.hasError로 에러 유무만 확인하는 것을 넘어, 발생한 에러의 타입에 따라 다른 UI를 보여줄 수 있습니다. 예를 들어, 네트워크 연결 문제(SocketException)와 서버가 보낸 에러(HttpException)를 구분하여 사용자에게 더 구체적인 안내를 제공할 수 있습니다.
builder: (context, snapshot) {
if (snapshot.hasError) {
final error = snapshot.error;
if (error is TimeoutException) {
return const Center(child: Text('Request timed out. Please try again.'));
} else if (error is SocketException) {
return const Center(child: Text('No internet connection.'));
} else {
// 기타 예외 처리 및 로그 기록
// FirebaseCrashlytics.instance.recordError(error, snapshot.stackTrace);
return const Center(child: Text('An unexpected error occurred.'));
}
}
// ... 성공 및 로딩 처리
}
상태 관리 라이브러리와의 통합
앞서 언급했듯이, StatefulWidget의 initState에서 비동기 작업을 관리하는 것은 간단한 경우에 효과적입니다. 하지만 앱의 규모가 커지고 상태가 복잡해지면, 상태 관리 라이브러리를 사용하는 것이 훨씬 효율적입니다. 예를 들어, provider 패키지의 FutureProvider나 StreamProvider를 사용하면 UI 코드와 비즈니스 로직을 깔끔하게 분리할 수 있습니다.
FutureProvider는 Future를 제공하고 그 결과를 하위 위젯에서 쉽게 소비할 수 있도록 캐싱해줍니다. 이를 통해 FutureBuilder와 initState 로직을 직접 작성할 필요가 없어집니다.
// main.dart
void main() {
runApp(
// 1. FutureProvider로 비동기 데이터를 제공
FutureProvider<String>(
create: (_) => fetchUserName(),
initialData: 'Loading...',
child: const MyApp(),
),
);
}
// my_app.dart
class UserNameDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 2. Consumer 또는 context.watch로 데이터를 소비
final userName = context.watch<String>();
return Text('Welcome, $userName');
}
}
이러한 접근 방식은 코드의 재사용성을 높이고 테스트를 용이하게 만들어줍니다.
실전 예제: 실제 앱 시나리오 적용
이론을 실제 코드로 옮겨보겠습니다. 두 가지 구체적인 시나리오를 통해 각 위젯의 활용법을 살펴보겠습니다.
사례 1: JSONPlaceholder API로 포스트 목록 로딩 (FutureBuilder)
공개 API를 호출하여 게시물 목록을 가져와 화면에 표시하는 전형적인 예제입니다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// 1. 데이터 모델 정의
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
// 2. API 호출 함수 정의
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((post) => Post.fromJson(post)).toList();
} else {
throw Exception('Failed to load posts from API');
}
}
// 3. StatefulWidget과 FutureBuilder로 UI 구성
class PostListPage extends StatefulWidget {
const PostListPage({super.key});
@override
State<PostListPage> createState() => _PostListPageState();
}
class _PostListPageState extends State<PostListPage> {
late final Future<List<Post>> _postsFuture;
@override
void initState() {
super.initState();
_postsFuture = fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('API Posts')),
body: FutureBuilder<List<Post>>(
future: _postsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text("Error: ${snapshot.error}"));
} else if (snapshot.hasData) {
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(posts[index].title),
subtitle: Text(posts[index].body, maxLines: 2, overflow: TextOverflow.ellipsis),
);
},
);
} else {
return const Center(child: Text("No posts found."));
}
},
),
);
}
}
사례 2: Firebase Firestore로 실시간 채팅 구현 (StreamBuilder)
StreamBuilder의 강력함은 Firebase와 같은 실시간 데이터베이스와 결합될 때 극대화됩니다. Firestore 컬렉션의 변경 사항을 실시간으로 스트리밍하여 UI에 반영할 수 있습니다.
아래는 Firestore의 `messages` 컬렉션을 구독하여 새로운 메시지가 추가될 때마다 화면에 즉시 표시하는 예제 코드 스니펫입니다. (Firebase 설정이 필요합니다.)
// Firebase Core 및 Firestore 패키지가 필요합니다.
// import 'package:cloud_firestore/cloud_firestore.dart';
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context) {
// 1. Firestore의 'messages' 컬렉션을 시간순으로 정렬하여 스트림을 가져옵니다.
final Stream<QuerySnapshot> messagesStream = FirebaseFirestore.instance
.collection('messages')
.orderBy('timestamp', descending: true)
.snapshots();
return Scaffold(
appBar: AppBar(title: const Text('Real-time Chat')),
body: StreamBuilder<QuerySnapshot>(
stream: messagesStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Center(child: Text('Something went wrong'));
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 2. 스트림에서 받은 데이터(문서 목록)로 리스트뷰를 만듭니다.
return ListView(
reverse: true, // 최신 메시지가 아래에 보이도록
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['sender'] ?? 'Unknown User'),
subtitle: Text(data['text'] ?? ''),
);
}).toList(),
);
},
),
// ... 메시지 입력 필드 UI ...
);
}
}
이처럼 StreamBuilder를 사용하면 복잡한 콜백이나 상태 관리 로직 없이도 단 몇 줄의 코드로 강력한 실시간 기능을 구현할 수 있습니다.
결론: 올바른 비동기 위젯 선택하기
FutureBuilder와 StreamBuilder는 Flutter에서 비동기 데이터를 UI에 통합하는 데 없어서는 안 될 핵심 위젯입니다. 두 위젯의 선택은 매우 간단한 기준에 따라 결정됩니다.
- 단 한 번의 비동기 결과가 필요하다면? →
FutureBuilder를 사용하세요. - 시간이 지남에 따라 변하는 연속적인 데이터가 필요하다면? →
StreamBuilder를 사용하세요.
무엇보다 중요한 것은 build 메소드가 여러 번 호출될 수 있다는 Flutter의 특성을 이해하고, 불필요한 비동기 작업 재실행을 방지하기 위해 Future나 Stream 객체의 상태를 올바르게 관리하는 것입니다. StatefulWidget의 생명주기를 활용하거나 상태 관리 라이브러리를 도입하여 이러한 문제를 해결함으로써, 안정적이고 성능이 뛰어난 반응형 애플리케이션을 구축할 수 있을 것입니다.
0 개의 댓글:
Post a Comment