Flutter에서 비동기 프로그래밍: Future와 Stream 🚀
안녕하세요, 플러터 개발자 여러분! 오늘은 정말 핫한 주제인 "Flutter에서 비동기 프로그래밍: Future와 Stream"에 대해 깊이 파헤쳐볼 거예요. 이 글을 읽고 나면 여러분도 비동기 프로그래밍의 고수가 될 수 있을 거예요! 😎
먼저, 비동기 프로그래밍이 뭔지 아시나요? 간단히 말하면, 코드가 순차적으로 실행되지 않고 여러 작업을 동시에 처리할 수 있게 해주는 프로그래밍 방식이에요. 특히 모바일 앱 개발에서는 정말 중요한 개념이죠. 왜냐구요? 사용자 경험을 개선하고 앱의 성능을 높이는 데 큰 도움이 되거든요!
Flutter에서는 비동기 프로그래밍을 위해 주로 Future와 Stream이라는 두 가지 핵심 개념을 사용해요. 이 두 친구들은 비동기 작업을 처리하는 데 있어 각자의 역할이 있답니다. 오늘은 이 두 개념에 대해 자세히 알아보고, 실제로 어떻게 사용하는지 예제 코드와 함께 살펴볼 거예요.
그럼 지금부터 Flutter의 비동기 세계로 떠나볼까요? 준비되셨나요? 레츠고! 🚀
1. Future: 미래의 결과를 기다리는 마법 ✨
자, 여러분! Future에 대해 들어보셨나요? Future는 말 그대로 '미래'를 의미해요. 프로그래밍에서 Future는 비동기 작업의 결과를 나중에 받을 수 있는 객체를 말합니다. 쉽게 말해, "지금은 결과가 없지만, 나중에 결과가 올 거야!"라고 약속하는 거죠.
예를 들어볼까요? 여러분이 카페에서 커피를 주문했다고 생각해보세요. 주문을 하고 바로 커피가 나오나요? 아니죠! 주문을 하고 나면 바리스타가 커피를 만드는 동안 여러분은 다른 일을 할 수 있어요. 커피가 완성되면 바리스타가 여러분을 부르겠죠. 이게 바로 Future의 개념이에요!
Flutter에서 Future는 주로 네트워크 요청, 파일 읽기/쓰기, 데이터베이스 작업 등 시간이 걸리는 작업을 처리할 때 사용해요. 이런 작업들은 앱의 메인 스레드를 블로킹하지 않고 백그라운드에서 실행되어야 하거든요.
Future 사용하기
Future를 사용하는 방법은 크게 두 가지가 있어요:
- async/await 키워드 사용하기
- then() 메서드 사용하기
두 방법 모두 알아둬야 해요. 상황에 따라 더 적합한 방법을 선택할 수 있거든요. 자, 이제 각각의 방법을 자세히 살펴볼까요?
1) async/await 키워드 사용하기
async/await는 비동기 코드를 마치 동기 코드처럼 쉽게 작성할 수 있게 해주는 Flutter의 마법 같은 기능이에요. 어떻게 사용하는지 볼까요?
Future<String> fetchUserOrder() async {
// 네트워크 요청을 시뮬레이션하는 지연
await Future.delayed(Duration(seconds: 2));
return '아이스 아메리카노';
}
void main() async {
print('커피 주문을 시작합니다.');
String order = await fetchUserOrder();
print('주문하신 $order가 준비되었습니다!');
}
이 코드에서 async 키워드는 함수가 비동기 함수임을 나타내요. await 키워드는 Future가 완료될 때까지 기다리라고 지시하는 거죠. 이렇게 하면 비동기 코드를 마치 동기 코드처럼 읽기 쉽게 작성할 수 있어요.
실행 결과는 이렇게 나올 거예요:
커피 주문을 시작합니다.
(2초 후)
주문하신 아이스 아메리카노가 준비되었습니다!
어때요? 코드가 순서대로 실행되는 것처럼 보이지만, 실제로는 2초 동안 다른 작업을 할 수 있답니다. 이게 바로 비동기 프로그래밍의 매력이에요! 😍
2) then() 메서드 사용하기
then() 메서드는 Future의 결과를 처리하는 또 다른 방법이에요. async/await보다는 조금 복잡해 보일 수 있지만, 특정 상황에서는 더 유용할 수 있어요.
Future<String> fetchUserOrder() {
return Future.delayed(Duration(seconds: 2), () => '아이스 라떼');
}
void main() {
print('커피 주문을 시작합니다.');
fetchUserOrder().then((order) {
print('주문하신 $order가 준비되었습니다!');
}).catchError((error) {
print('주문 중 오류가 발생했습니다: $error');
});
print('주문 후 다른 작업을 할 수 있어요.');
}
이 방식에서는 then() 메서드를 사용해 Future의 결과를 처리해요. catchError() 메서드는 오류가 발생했을 때 처리하는 방법을 제공하죠.
실행 결과는 이렇게 나와요:
커피 주문을 시작합니다.
주문 후 다른 작업을 할 수 있어요.
(2초 후)
주문하신 아이스 라떼가 준비되었습니다!
보셨나요? then() 메서드를 사용하면 비동기 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행할 수 있어요. 이게 바로 비동기 프로그래밍의 강점이죠!
Future의 다양한 활용
Future는 정말 다양한 상황에서 활용할 수 있어요. 몇 가지 예를 더 살펴볼까요?
1) 여러 개의 Future 동시에 처리하기
때로는 여러 개의 비동기 작업을 동시에 처리해야 할 때가 있어요. 이럴 때 Future.wait()를 사용하면 편리해요.
Future<String> fetchCoffee() => Future.delayed(Duration(seconds: 2), () => '커피');
Future<String> fetchCroissant() => Future.delayed(Duration(seconds: 3), () => '크루아상');
void main() async {
print('주문을 시작합니다.');
var results = await Future.wait([fetchCoffee(), fetchCroissant()]);
print('주문하신 ${results[0]}와 ${results[1]}이 준비되었습니다!');
}
이 코드는 커피와 크루아상을 동시에 주문하고, 둘 다 준비될 때까지 기다려요. 실행 결과는 이렇게 나와요:
주문을 시작합니다.
(3초 후)
주문하신 커피와 크루아상이 준비되었습니다!
3초만에 두 가지 작업이 모두 완료되었네요! 👏
2) Future 체이닝
때로는 한 Future의 결과를 바탕으로 다른 Future를 실행해야 할 때가 있어요. 이럴 때는 Future 체이닝을 사용할 수 있어요.
Future<String> fetchUserId() => Future.delayed(Duration(seconds: 1), () => 'user123');
Future<String> fetchUserName(String userId) => Future.delayed(Duration(seconds: 1), () => '김플러터');
void main() {
print('사용자 정보를 가져오는 중...');
fetchUserId()
.then((userId) => fetchUserName(userId))
.then((userName) => print('환영합니다, $userName님!'))
.catchError((error) => print('오류 발생: $error'));
}
이 코드는 먼저 사용자 ID를 가져오고, 그 ID를 사용해 사용자 이름을 가져와요. 실행 결과는 이렇게 나와요:
사용자 정보를 가져오는 중...
(2초 후)
환영합니다, 김플러터님!
Future 체이닝을 사용하면 복잡한 비동기 작업도 깔끔하게 처리할 수 있어요. 👍
Future의 장단점
자, 이제 Future에 대해 꽤 많이 알게 되셨죠? Future는 정말 유용한 도구지만, 모든 것이 그렇듯 장단점이 있어요. 한번 살펴볼까요?
장점 👍
- 간단한 비동기 처리: Future를 사용하면 복잡한 비동기 작업을 간단하게 처리할 수 있어요.
- 코드 가독성 향상: async/await를 사용하면 비동기 코드를 마치 동기 코드처럼 읽기 쉽게 작성할 수 있어요.
- 에러 처리 용이: catchError() 메서드나 try-catch 문을 사용해 쉽게 에러를 처리할 수 있어요.
- 성능 향상: 비동기 처리를 통해 앱의 반응성을 높일 수 있어요.
단점 👎
- 복잡성 증가: 많은 비동기 작업을 다룰 때 코드가 복잡해질 수 있어요.
- 디버깅의 어려움: 비동기 코드는 동기 코드에 비해 디버깅이 조금 더 어려울 수 있어요.
- 상태 관리의 복잡성: 여러 Future를 동시에 관리할 때 상태 관리가 복잡해질 수 있어요.
하지만 걱정 마세요! 이런 단점들은 경험이 쌓이면서 자연스럽게 극복할 수 있답니다. 그리고 이런 단점들을 보완하기 위해 Stream이라는 또 다른 도구가 있어요. 다음 섹션에서 자세히 알아보도록 할게요! 😉
여기까지 Future에 대해 알아봤어요. 어떠신가요? 비동기 프로그래밍의 세계가 조금은 친숙해지셨나요? Future는 Flutter 개발에서 정말 중요한 개념이에요. 특히 네트워크 요청이나 데이터베이스 작업 같은 시간이 걸리는 작업을 할 때 꼭 필요하답니다.
그런데 말이죠, Future만으로는 부족한 경우가 있어요. 예를 들어, 실시간으로 계속해서 데이터를 받아와야 하는 경우에는 어떻게 해야 할까요? 바로 이럴 때 Stream이 필요한 거예요! 다음 섹션에서는 Stream에 대해 자세히 알아보도록 할게요. 준비되셨나요? 그럼 고고! 🚀
2. Stream: 데이터의 끊임없는 흐름 🌊
안녕하세요, 플러터 개발자 여러분! 이제 우리의 두 번째 주인공인 Stream에 대해 알아볼 시간이에요. Stream은 Future와 마찬가지로 비동기 프로그래밍에서 중요한 역할을 하지만, 조금 다른 방식으로 동작해요. 자, 그럼 Stream의 세계로 빠져볼까요? 🏊♂️
Stream이란?
Stream은 말 그대로 '흐름'을 의미해요. 프로그래밍에서 Stream은 시간에 따라 연속적으로 들어오는 데이터의 흐름을 나타내요. Future가 단 한 번의 결과를 제공한다면, Stream은 여러 번에 걸쳐 지속적으로 데이터를 제공할 수 있어요.
쉽게 이해하기 위해 예를 들어볼게요. Future를 커피 주문에 비유했다면, Stream은 뭐라고 비유할 수 있을까요? 음... 유튜브 라이브 스트리밍이라고 생각해보면 어떨까요? 🎥
유튜브 라이브 스트리밍을 보면, 영상이 끊임없이 계속해서 전송되죠? 시청자는 스트리밍이 시작되면 실시간으로 계속 새로운 영상 프레임을 받아볼 수 있어요. 이것이 바로 Stream의 개념과 비슷해요!
Flutter에서 Stream은 주로 다음과 같은 상황에서 사용돼요:
- 실시간 데이터 처리 (예: 채팅 앱, 주식 시세 앱)
- 파일 읽기/쓰기
- 센서 데이터 모니터링
- 애니메이션 제어
- 상태 관리
자, 이제 Stream이 뭔지 대충 감이 오시나요? 그럼 이제 Stream을 어떻게 사용하는지 자세히 알아볼게요!
Stream 사용하기
Stream을 사용하는 방법은 크게 두 가지예요:
- async* 함수와 yield 키워드 사용하기
- StreamController 사용하기
각각의 방법에 대해 자세히 알아볼게요. 준비되셨나요? 고고! 🚀
1) async* 함수와 yield 키워드 사용하기
async* 함수는 Stream을 생성하는 가장 간단한 방법이에요. yield 키워드는 Stream에 값을 추가할 때 사용해요. 어떻게 사용하는지 예제를 통해 알아볼까요?
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
print('카운트다운을 시작합니다!');
await for (int i in countStream(5)) {
print(i);
}
print('카운트다운 종료!');
}
이 코드에서 async*는 이 함수가 Stream을 생성한다는 것을 나타내요. yield 키워드는 Stream에 값을 추가해요. 메인 함수에서는 await for 루프를 사용해 Stream의 각 값을 기다리고 처리해요.
실행 결과는 이렇게 나와요:
카운트다운을 시작합니다!
1
2
3
4
5
카운트다운 종료!
와우! 1초 간격으로 숫자가 출력되는 걸 볼 수 있어요. 이게 바로 Stream의 매력이죠! 😍
2) StreamController 사용하기
StreamController는 Stream을 더 세밀하게 제어할 수 있게 해주는 도구예요. 데이터를 추가하거나, Stream을 닫거나, 에러를 추가하는 등의 작업을 할 수 있어요.
import 'dart:async';
void main() {
final controller = StreamController<String>();
// Stream 리스닝
controller.stream.listen(
(data) => print('받은 데이터: $data'),
onError: (error) => print('에러 발생: $error'),
onDone: () => print('Stream 종료'),
);
// 데이터 추가
controller.add('안녕하세요');
controller.add('Flutter');
controller.add('Stream');
// 에러 추가
controller.addError('이런! 에러가 발생했어요!');
// Stream 종료
controller.close();
}
이 코드에서 StreamController를 생성하고, 그 Stream에 리스너를 추가해요. 그리고 controller.add()를 사용해 데이터를 추가하고, addError()로 에러를 추가하고, 마지막으로 close()로 Stream을 종료해요.
실행 결과는 이렇게 나와요:
받은 데이터: 안녕하세요
받은 데이터: Flutter
받은 데이터: Stream
에러 발생: 이런! 에러가 발생했어요!
Stream 종료
StreamController를 사용하면 이렇게 Stream을 더 유연하게 다룰 수 있어요. 멋지죠? 😎
Stream의 다양한 활용
Stream은 정말 다양한 상황에서 활용할 수 있어요. 몇 가지 예를 더 살펴볼까요?
1) Stream 변환하기
Stream의 데이터를 변환하고 싶을 때는 map() 메서드를 사용할 수 있어요.
Stream<int> numberStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
var stream = numberStream().map((number) => '숫자: $number');
await for (var value in stream) {
print(value);
}
}
이 코드는 숫자 Stream을 문자열 Stream으로 변환해요. 실행 결과는 이렇게 나와요:
숫자: 1
숫자: 2
숫자: 3
숫자: 4
숫자: 5
이렇게 Stream의 데이터를 원하는 형태로 쉽게 변환할 수 있어요. 편리하죠? 👍
2) Stream 필터링하기
Stream에서 특정 조건을 만족하는 데이터만 선택하고 싶을 때는 where() 메서드를 사용할 수 있어요.
Stream<int> numberStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
var stream = numberStream().where((number) => number % 2 == 0);
await for (var value in stream) {
print('짝수: $value');
}
}
이 코드는 1부터 10까지의 숫자 중에서 짝수만 선택해요. 실행 결과는 이렇게 나와요:
짝수: 2
짝수: 4
짝수: 6
짝수: 8
짝수: 10
이렇게 Stream에서 원하는 데이터만 쉽게 필터링할 수 있어요. 유용하죠? 😊
Stream의 장단점
자, 이제 Stream에 대해 꽤 많이 알게 되셨죠? Stream도 Future와 마찬가지로 장단점이 있어요. 한번 살펴볼까요?
장점 👍
- 실시간 데이터 처리: Stream을 사용하면 실시간으로 들어오는 데이터를 효과적으로 처리할 수 있어요.
- 메모리 효율성: 대량의 데이터를 한 번에 처리하지 않고 조금씩 처리할 수 있어 메모리를 효율적으로 사용할 수 있어요.
- 반응형 프로그래밍: Stream은 반응형 프로그래밍을 가능하게 해, 사용자 인터페이스를 더 동적으로 만들 수 있어요.
- 유연성: Stream은 다양한 연산자(map, where, take 등)를 제공해 데이터를 쉽게 변형하고 필터링할 수 있어요.
단점 👎
- 복잡성: Stream은 Future보다 개념적으로 더 복잡할 수 있어요. 처음 접하는 개발자들에게는 이해하기 어려울 수 있죠.
- 오버헤드: 간단한 작업에 Stream을 사용하면 불필요한 복잡성과 오버헤드가 생길 수 있어요.
- 리소스 관리: Stream을 제대로 관리하지 않으면 메모리 누수가 발생할 수 있어요. Stream을 사용한 후에는 반드시 닫아주어야 해요.
하지만 걱정 마세요! 이런 단점들도 경험이 쌓이면서 자연스럽게 극복할 수 있답니다. 그리고 Stream의 장점이 단점을 훨씬 뛰어넘는 경우가 많아 요. 특히 실시간 데이터를 다루는 앱을 개발할 때는 Stream의 강력함을 실감하실 수 있을 거예요! 😉
Future vs Stream: 언제 무엇을 사용해야 할까?
자, 이제 Future와 Stream에 대해 모두 알아봤어요. 그런데 "언제 Future를 사용하고, 언제 Stream을 사용해야 할까요?"라는 의문이 들 수 있어요. 이에 대해 간단히 정리해볼게요:
Future를 사용하는 경우:
- 단일 비동기 작업의 결과를 기다릴 때 (예: 한 번의 API 호출)
- 파일을 한 번 읽거나 쓸 때
- 데이터베이스에서 한 번 쿼리를 실행할 때
Stream을 사용하는 경우:
- 연속적인 데이터를 처리할 때 (예: 실시간 채팅 메시지)
- 센서 데이터를 지속적으로 모니터링할 때
- 파일의 내용을 점진적으로 읽을 때
- 사용자 인터페이스의 상태 변화를 관찰할 때
간단히 말해, 한 번의 결과만 필요하다면 Future를, 연속적인 데이터 흐름이 필요하다면 Stream을 사용하면 돼요. 물론 상황에 따라 둘 다 사용할 수도 있겠죠? 😊
실전 예제: 주식 가격 모니터링 앱
자, 이제 우리가 배운 내용을 활용해서 간단한 주식 가격 모니터링 앱을 만들어볼까요? 이 앱은 Future와 Stream을 모두 사용할 거예요.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StockPage(),
);
}
}
class StockPage extends StatefulWidget {
@override
_StockPageState createState() => _StockPageState();
}
class _StockPageState extends State<StockPage> {
String stockSymbol = '';
double currentPrice = 0.0;
StreamSubscription? priceSubscription;
Future<void> fetchInitialPrice() async {
// 실제로는 API를 호출해야 하지만, 여기서는 시뮬레이션합니다.
await Future.delayed(Duration(seconds: 2));
setState(() {
currentPrice = 100.0; // 초기 가격
});
}
Stream<double> getPriceStream() async* {
// 실제로는 실시간 API를 사용해야 하지만, 여기서는 시뮬레이션합니다.
while (true) {
await Future.delayed(Duration(seconds: 1));
yield currentPrice + (Random().nextDouble() - 0.5) * 2;
}
}
@override
void initState() {
super.initState();
fetchInitialPrice();
}
void startMonitoring() {
priceSubscription = getPriceStream().listen((price) {
setState(() {
currentPrice = price;
});
});
}
void stopMonitoring() {
priceSubscription?.cancel();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('주식 모니터링')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('현재 가격: \$${currentPrice.toStringAsFixed(2)}'),
SizedBox(height: 20),
ElevatedButton(
onPressed: startMonitoring,
child: Text('모니터링 시작'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: stopMonitoring,
child: Text('모니터링 중지'),
),
],
),
),
);
}
@override
void dispose() {
priceSubscription?.cancel();
super.dispose();
}
}
이 예제에서는 Future를 사용해 초기 주식 가격을 가져오고, Stream을 사용해 실시간으로 변하는 주식 가격을 모니터링해요. 사용자는 버튼을 눌러 모니터링을 시작하거나 중지할 수 있어요.
이 앱을 실행하면, 초기에 Future를 통해 가격을 가져오고, '모니터링 시작' 버튼을 누르면 Stream을 통해 실시간으로 변하는 가격을 볼 수 있어요. 멋지죠? 😎
마무리
여기까지 Flutter에서의 비동기 프로그래밍, 특히 Future와 Stream에 대해 자세히 알아봤어요. 어떠셨나요? 처음에는 조금 복잡해 보일 수 있지만, 실제로 사용해보면 정말 강력하고 유용한 도구라는 걸 느끼실 수 있을 거예요.
Future와 Stream을 잘 활용하면, 사용자에게 더 나은 경험을 제공하는 반응성 높은 앱을 만들 수 있어요. 네트워크 요청, 파일 처리, 실시간 데이터 처리 등 다양한 상황에서 이 개념들을 적용할 수 있죠.
물론, 이 개념들을 완전히 이해하고 자유자재로 사용하기까지는 시간이 걸릴 수 있어요. 하지만 걱정하지 마세요! 계속 연습하고 실제 프로젝트에 적용해보면서 경험을 쌓다 보면, 어느새 비동기 프로그래밍의 달인이 되어 있을 거예요. 💪
앞으로도 계속해서 Flutter와 Dart의 다양한 기능들을 탐험해보세요. 여러분의 Flutter 개발 여정에 행운이 함께하기를 바랍니다. 화이팅! 🚀