이번 포스팅에서는 Dart 언어의 두 가지 중요한 고급 기능인 비동기 프로그래밍과 제네릭에 대해 알아보겠습니다. 이 기능들은 효율적이고 유연한 코드를 작성하는 데 큰 도움이 됩니다.
동기 프로그래밍 vs 비동기 프로그래밍
동기 프로그래밍
동기 프로그래밍에서는 코드가 순차적으로 실행됩니다. 각 작업은 이전 작업이 완료될 때까지 기다린 후 실행됩니다. 이는 간단하고 직관적이지만, 시간이 오래 걸리는 작업(예: 파일 읽기, 네트워크 요청)이 있을 경우 전체 프로그램의 실행이 blocked 되어 사용자 경험이 저하될 수 있습니다.
예시:
void main() {
print('작업 1 시작');
longRunningTask(); // 이 작업이 완료될 때까지 다음 줄로 넘어가지 않습니다.
print('작업 2 시작');
}
void longRunningTask() {
// 시간이 오래 걸리는 작업
}
비동기 프로그래밍
비동기 프로그래밍에서는 시간이 오래 걸리는 작업을 “백그라운드”로 보내고, 그 동안 다른 코드를 계속 실행할 수 있습니다. 작업이 완료되면 그 결과를 처리합니다. 이 방식은 프로그램의 반응성을 높이고 자원을 효율적으로 사용할 수 있게 해줍니다.
예시:
void main() async {
print('작업 1 시작');
longRunningTask().then((_) => print('긴 작업 완료'));
print('작업 2 시작'); // longRunningTask가 완료되기 전에 실행됩니다.
}
Future<void> longRunningTask() async {
// 시간이 오래 걸리는 작업
}
비동기 프로그래밍의 이점:
- better performance: 여러 작업을 동시에 처리할 수 있습니다.
- improved responsiveness: 사용자 인터페이스가 blocking 되지 않습니다.
- efficient resource utilization: CPU와 I/O 리소스를 더 효율적으로 사용할 수 있습니다.
Dart는 Future
, async
, await
등의 키워드를 제공하여 비동기 프로그래밍을 쉽게 구현할 수 있게 해줍니다. 이제 이러한 개념들을 자세히 살펴보겠습니다.
1. 비동기 프로그래밍
비동기 프로그래밍은 시간이 걸리는 작업(예: 네트워크 요청, 파일 읽기/쓰기)을 처리할 때 프로그램이 멈추지 않고 계속 실행될 수 있게 해줍니다.
Future
Future
는 비동기 연산의 결과를 나타내는 객체입니다.
Future<String> fetchUserOrder() {
return Future.delayed(Duration(seconds: 2), () => 'Large Latte');
}
void main() {
print('Fetching user order...');
fetchUserOrder().then((order) => print('Your order is: $order'));
print('This runs before the order is fetched!');
}
async와 await
async
와 await
키워드를 사용하면 비동기 코드를 더 쉽게 작성할 수 있습니다.
Future<void> main() async {
print('Fetching user order...');
String order = await fetchUserOrder();
print('Your order is: $order');
}
Stream
Stream
은 비동기 이벤트의 시퀀스를 나타냅니다.
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
yield i;
await Future.delayed(Duration(seconds: 1));
}
}
void main() async {
Stream<int> stream = countStream(5);
await for (int number in stream) {
print(number);
}
}
2. 제네릭
제네릭을 사용하면 타입에 상관없이 재사용 가능한 코드를 작성할 수 있습니다.
제네릭 클래스
class Box<T> {
T value;
Box(this.value);
T getValue() => value;
void setValue(T value) => this.value = value;
}
void main() {
var intBox = Box<int>(42);
var stringBox = Box<String>('Hello, Dart!');
print(intBox.getValue()); // 출력: 42
print(stringBox.getValue()); // 출력: Hello, Dart!
}
제네릭 함수
T first<T>(List<T> list) {
if (list.isEmpty) throw Exception('List is empty');
return list[0];
}
void main() {
var numbers = [1, 2, 3, 4, 5];
var strings = ['one', 'two', 'three'];
print(first(numbers)); // 출력: 1
print(first(strings)); // 출력: one
}
제네릭 제약
특정 타입이나 그 하위 타입만 사용하도록 제한할 수 있습니다.
class Animal {
void makeSound() => print('Some generic animal sound');
}
class Dog extends Animal {
@override
void makeSound() => print('Woof!');
}
void makeAnimalSound<T extends Animal>(T animal) {
animal.makeSound();
}
void main() {
var animal = Animal();
var dog = Dog();
makeAnimalSound(animal); // 출력: Some generic animal sound
makeAnimalSound(dog); // 출력: Woof!
}
마무리
이번 포스팅에서는 Dart의 고급 기능인 비동기 프로그래밍과 제네릭에 대해 알아보았습니다. 이러한 기능들은 복잡한 애플리케이션을 개발할 때 코드의 효율성과 재사용성을 크게 향상시킵니다.
다음 포스팅에서는 Dart를 사용하여 Flutter로 모바일 앱 개발을 시작하는 방법에 대해 다루겠습니다.
Dart 언어에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!
관련 리소스
더 자세한 정보를 원하시면 아래의 공식 홈페이지를 참조하세요:
- Dart 공식 홈페이지: https://dart.dev/
- Flutter 공식 홈페이지: https://flutter.dev/