출시·고도화 중
Dart 안내서 · 6/6
이 장에서는 Dart를 Dart답게 만드는 개념을 모아 살펴봅니다. Future와 async/await, Stream으로 대표되는 비동기 프로그래밍, 메모리를 공유하지 않는 동시성 단위인 아이솔레이트(isolate), 코드 재사용을 위한 믹스인과 확장 메서드, 그리고 이 모든 것이 Flutter에서 어떻게 쓰이는지입니다.
Dart 코드는 하나의 아이솔레이트 안에서 단일 스레드 이벤트 루프로 실행됩니다. 파일 읽기나 네트워크 요청처럼 오래 걸리는 작업은 결과를 기다리며 멈추지 않고, 나중에 값을 줄 것을 약속하는 Future를 바로 돌려줍니다. async 함수 안에서 await를 쓰면 그 Future가 끝날 때까지 기다린 것처럼 순서대로 코드를 쓸 수 있고, 기다리는 동안 이벤트 루프는 다른 일을 처리합니다.
Future<String> fetchUser(int id) async {
await Future.delayed(const Duration(milliseconds: 300)); // 네트워크 흉내
return 'user$id';
}
Future<void> main() async {
print('시작');
final user = await fetchUser(1);
print(user);
// 여러 작업을 동시에 시작하고 모두 끝날 때까지 기다리기
final users = await Future.wait([fetchUser(2), fetchUser(3)]);
print(users); // [user2, user3]
// 시간 제한
try {
await fetchUser(4).timeout(const Duration(milliseconds: 100));
} catch (e) {
print('시간 초과: $e');
}
}async 함수는 항상 Future를 돌려줍니다. 돌려줄 값이 없으면 Future<void>로 선언합니다. await 없이 Future를 호출만 하고 버리면 오류가 조용히 묻힐 수 있으므로, 의도적으로 기다리지 않는 경우에는 dart:async의 unawaited()로 그 의도를 드러냅니다.
Future가 값 하나라면 Stream은 시간에 따라 차례로 도착하는 값의 연속입니다. 파일을 조각으로 읽거나, 웹소켓 메시지, 사용자 입력 이벤트를 다룰 때 씁니다. async* 함수에서 yield로 값을 내보내 스트림을 만들 수 있고, await for로 하나씩 받거나 listen으로 구독합니다.
Stream<int> countdown(int from) async* {
for (var i = from; i > 0; i--) {
await Future.delayed(const Duration(milliseconds: 200));
yield i;
}
}
Future<void> main() async {
await for (final n in countdown(3)) {
print(n); // 3, 2, 1
}
final evensSquared = countdown(6)
.where((n) => n.isEven)
.map((n) => n * n);
final subscription = evensSquared.listen(
(value) => print('값: $value'),
onError: (Object e) => print('오류: $e'),
onDone: () => print('끝'),
);
await subscription.asFuture<void>();
}스트림에는 한 번만 구독할 수 있는 단일 구독(single-subscription) 스트림과 여러 곳에서 동시에 들을 수 있는 브로드캐스트 스트림이 있습니다. 직접 이벤트를 넣어야 할 때는 StreamController를 씁니다.
async/await는 기다리는 동안 다른 일을 하게 해 줄 뿐, CPU를 오래 쓰는 계산을 다른 코어로 옮기지는 않습니다. 큰 JSON을 해석하거나 이미지를 처리하는 무거운 계산은 아이솔레이트에서 실행합니다. 아이솔레이트는 자기만의 메모리와 이벤트 루프를 가진 실행 단위이고, 다른 아이솔레이트와 메모리를 공유하지 않으며 메시지로만 통신합니다. 그래서 잠금(lock) 없이도 데이터 경쟁이 생기지 않습니다.
import 'dart:isolate';
int fib(int n) => n < 2 ? n : fib(n - 1) + fib(n - 2);
Future<void> main() async {
// 새 아이솔레이트에서 계산하고 결과를 받아 옵니다
final result = await Isolate.run(() => fib(38));
print(result);
}Isolate.run은 함수를 새 아이솔레이트에서 실행하고 결과를 Future로 돌려줍니다. 오래 살아 있으며 여러 메시지를 주고받는 작업자가 필요하면 Isolate.spawn과 ReceivePort, SendPort를 씁니다. Flutter에서는 같은 목적으로 compute 함수도 쓸 수 있습니다. 웹으로 컴파일한 Dart는 아이솔레이트 대신 웹 워커 같은 브라우저 기능을 써야 한다는 점도 알아 둡니다.
Dart는 단일 상속이지만, 믹스인으로 여러 클래스에 같은 기능을 끼워 넣을 수 있습니다. mixin으로 선언하고 with로 적용합니다. on을 쓰면 특정 타입의 하위 클래스에만 쓸 수 있게 제한할 수 있습니다.
확장 메서드는 소스를 고칠 수 없는 기존 타입(예: String, List)에 메서드나 게터를 더합니다. 확장은 정적으로 결정되므로 변수의 정적 타입이 dynamic이면 적용되지 않습니다.
abstract class Animal {
String get name;
}
mixin Swimmer on Animal {
void swim() => print('$name 이(가) 헤엄칩니다');
}
mixin Flyer {
void fly() => print('날아오릅니다');
}
class Duck extends Animal with Swimmer, Flyer {
@override
final String name = '오리';
}
extension StringChecks on String {
bool get isBlank => trim().isEmpty;
String truncate(int max) => length <= max ? this : '${substring(0, max)}…';
}
void main() {
Duck()
..swim()
..fly();
print(' '.isBlank); // true
print('Dart 언어 안내서'.truncate(4)); // Dart…
}Dart 3.3부터는 기존 타입을 실행 비용 없이 다른 정적 타입으로 감싸는 확장 타입(extension type)도 쓸 수 있습니다. 주로 JavaScript 상호 운용이나 ID 값에 별도 타입을 붙이는 용도로 씁니다.
Flutter의 화면은 모두 Dart 코드로 그립니다. 위젯은 Dart 클래스이고, 이름 있는 매개변수와 const 생성자 덕분에 위젯 트리를 읽기 쉽게 쓸 수 있으며, const 위젯은 다시 만들지 않아도 되어 성능에도 도움이 됩니다. Dart의 특성은 Flutter 개발 경험과 직접 연결됩니다.
| Dart의 특성 | Flutter에서 얻는 것 |
|---|---|
| 개발 중 JIT 컴파일 | 상태를 유지한 채 코드를 바꾸는 핫 리로드 |
| 배포 시 AOT 네이티브 컴파일 | 빠른 시작과 매끄러운 화면 |
| JavaScript · WebAssembly 컴파일 | 같은 코드로 웹 앱 배포 |
| 건전한 null 안전성 | 실행 중 null 오류 감소 |
async/await, 아이솔레이트 | 화면을 멈추지 않는 네트워크 · 계산 처리 |
Flutter를 쓰지 않더라도 Dart는 명령줄 도구, shelf 같은 패키지를 쓴 서버, 웹 앱에도 충분히 쓸 만한 언어입니다.
Dart는 단일 스레드 이벤트 루프 위에서 Future, async/await, Stream으로 비동기 작업을 다루고, 무거운 계산은 메모리를 공유하지 않는 아이솔레이트로 옮깁니다. 믹스인과 확장 메서드로 상속 없이 기능을 재사용하며, 이런 특성이 JIT와 AOT 컴파일과 어우러져 Flutter의 핫 리로드와 빠른 실행을 가능하게 합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.