Dart 2.18: Objective-C & Swift 인터롭
Dart 2.18: Objective-C & Swift 인터롭
향상된 상호 운용성, 플랫폼별 네트워킹, 개선된 타입 추론, 그리고 널 세이프티 언어 로드맵에 관한 중요한 업데이트 소식을 전해요. 오늘 Dart 2.18이 나왔습니다. 이번 릴리스에는 Objective-C & Swift 상호 운용성 프리뷰와, 그 인터롭을 바탕으로 만든 새 iOS/macOS 네트워킹 패키지가 포함돼 있어요. 또 제네릭 함수에 대한 개선된 타입 추론, async 코드의 성능 개선, 새 pub.dev 기능, 그리고 도구와 핵심 라이브러리의 정리도 담겨 있죠.
마지막으로, 최신 널 세이프티 이주 현황 수치와 완전한 널 세이프티 Dart로 나아가는 길에 관한 중요한 로드맵 업데이트도 준비했어요. 끝까지 읽어 주세요!
본문
Dart에서 Objective-C & Swift 인터롭 소개
우리는 2020년에 네이티브 C API를 호출하기 위한 Dart 외부 함수 인터페이스(FFI)를 프리뷰로 공개하고, 2021년 3월 Dart 2.12에서 정식 출시했어요. 그 이후로 많은 패키지가 이 기능을 활용해 기존 네이티브 C API와 통합했죠. file_picker, printing, win32, objectbox, realm, isar, tflite_flutter, dbus 같은 패키지가 몇 가지 예시예요.
Dart 팀은 Dart가 실행되는 플랫폼의 주요 언어들과 상호 운용될 수 있도록 지원하고 싶어요. Dart 2.18은 그 목표를 향한 다음 이정표를 달성해요. 이제 여러분의 Dart 코드가 Objective-C와 Swift 코드를 호출할 수 있어요. 이 언어들은 macOS와 iOS 플랫폼의 API에 주로 쓰이죠. Dart는 CLI 앱부터 백엔드 코드, Flutter UI에 이르기까지 어떤 앱에서도 이 인터롭 메커니즘을 지원해요.
이 새 메커니즘은 Objective-C와 Swift 코드가 API 바인딩을 기반으로 C 코드로 노출될 수 있다는 사실을 활용해요. Dart API 래퍼 생성 도구인 ffigen이 API 헤더에서 이런 바인딩을 만들 수 있죠. 예시를 하나 볼게요.
Objective-C를 쓰는 시간대(Time Zone) 예시
macOS에는 NSTimeZone 클래스에 노출된 시간대 정보를 조회하는 API가 있어요. 이 API로 사용자가 자기 기기에 설정한 시간대와 UTC 시간대 오프셋을 조회할 수 있어요.
다음 예시 Objective-C 앱은 이 시간대 API를 써서 시스템 시간대와 GMT 오프셋을 얻어요.
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSTimeZone *timezone = [NSTimeZone systemTimeZone]; // Get current time zone.
NSLog(@"Timezone name: %@", timezone.name);
NSLog(@"Timezone offset GMT: %ld hours", timezone.secondsFromGMT/60/60);
}
return 0;
}
이 앱은 Apple 파운데이션 라이브러리의 API 헤더를 담고 있는 Foundation.h를 가져와요. 그다음 main 메서드 안에서 NSTimeZone 클래스의 systemTimeZone 메서드를 호출해요. 이 메서드는 기기에서 선택된 시간대를 가진 NSTimeZone 인스턴스를 반환하죠. 마지막으로 앱은 시간대 이름과 UTC 오프셋(시간 단위)을 담은 두 줄을 콘솔에 출력해요.
이 앱을 실행하면, 위치에 따라 대략 아래와 같은 결과가 나와요.
Timezone name: Europe/Copenhagen
Timezone offset GMT: 2 hours
Dart를 쓰는 시간대(Time Zone) 예시
이번에는 새 Objective-C 인터롭으로 이 결과를 Dart에서 재현해 볼게요.
먼저 새 Dart CLI 앱을 만들어요.
$ dart create timezones
그다음 pubspec 파일을 편집해서 ffigen 설정을 담아요. 이 설정은 헤더 파일을 가리키고, 어떤 Objective-C 인터페이스의 래퍼를 생성할지 나열해요.
ffigen:
name: TimeZoneLibrary
language: objc
output: "foundation_bindings.dart"
exclude-all-by-default: true
objc-interfaces:
include:
- "NSTimeZone"
headers:
entry-points:
- "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/
Headers/NSTimeZone.h"
이 설정은 NSTimeZone.h의 헤더에 대한 Objective-C 바인딩을 선택하고, NSTimeZone 인터페이스의 API만 포함해요. 래퍼를 생성하려면 ffigen을 실행하면 돼요.
$ dart run ffigen
이 명령은 생성된 API 바인딩을 잔뜩 담은 새 파일 foundation_bindings.dart를 만들어요. 이 바인딩 파일을 쓰면 우리의 Dart main 메서드를 작성할 수 있어요. 이 메서드는 Objective-C 코드를 그대로 옮긴 거예요.
void main(List<String> args) async {
const dylibPath =
'/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
final lib = TimeZoneLibrary(DynamicLibrary.open(dylibPath));
final timeZone = NSTimeZone.getLocalTimeZone(lib);
if (timeZone != null) {
print('Timezone name: ${timeZone.name}');
print('Offset from GMT: ${timeZone.secondsFromGMT / 60 / 60} hours');
}
}
이게 전부예요! 이 새 지원은 오늘의 Dart 2.18부터 실험 상태로 사용할 수 있어요. 이로써 Dart의 일반적인 인터롭 지원이 macOS와 iOS API를 직접 호출하는 수준으로 올라갔어요. 이는 곧 Flutter 플러그인을 보완해서, 어떤 Dart 앱에서도 동작하는 새 지원으로 macOS와 iOS API를 Dart 코드에서 직접 호출할 수 있게 해 줘요.
피드백을 환영해요. 무엇이 잘 됐는지, 무엇이 바뀌어야 할지, 어떤 문제를 겪었는지 GitHub의 피드백 이슈에 댓글로 알려주세요. 이 상호 운용성에 대해 더 자세히 알고 싶다면 Objective-C and Swift interoperability guide를 확인해 보세요.
플랫폼별 http 라이브러리
Dart에는 일반적이고 멀티 플랫폼인 http 라이브러리가 있어요. 이 라이브러리를 쓰면 플랫폼 특정 사항을 신경 쓰지 않고 코드를 작성할 수 있어요. 때로는 특정 호스트 플랫폼의 네트워킹 API에 맞춘 코드를 쓰고 싶을 수도 있겠죠.
예를 들어 Apple의 네트워킹 라이브러리 NSURLSession은 WiFi 전용 네트워킹이나 VPN이 필요하다는 것을 지정할 수 있게 해 줘요. 이런 사용 사례를 지원하기 위해, 우리는 macOS와 iOS 플랫폼을 겨냥한 새 네트워킹 패키지 cupertino_http를 만들었어요. 이 패키지는 앞 절에서 언급한 새 Objective-C 인터롭을 바탕으로 해요. Foundation의 Apple 네트워킹 API에서 생성된 많은 API 래퍼를 사용하죠.
Cupertino http 라이브러리 예시
아래 예시는 Flutter 앱의 http 클라이언트가 macOS와 iOS에서는 cupertino_http 라이브러리를 쓰고, 다른 플랫폼에서는 dart:io의 일반 http 라이브러리를 쓰도록 설정해요.
late Client client;
if (Platform.isIOS || Platform.isMacOS) {
final config = URLSessionConfiguration.ephemeralSessionConfiguration()
..allowsCellularAccess = false
..allowsExpensiveNetworkAccess = false;
client = CupertinoClient.fromSessionConfiguration(config);
} else {
client = Client(); // Uses an HTTP client based on dart:io
}
이 초기 설정 이후에, 앱은 이후의 모든 네트워킹 호출을 특정 클라이언트로 수행해요. 예를 들어 http get() 요청은 이제 이렇게 생겼어요.
final response = await get(
Uri.https(
'www.googleapis.com',
'/books/v1/volumes',
{'q': 'HTTP', 'maxResults': '40', 'printType': 'books'},
),
);
공통 클라이언트 인터페이스를 쓸 수 없는 경우에는, cupertino_http 라이브러리로 Apple의 네트워킹 API를 직접 호출할 수 있어요.
final session = URLSession.sessionWithConfiguration(
URLSessionConfiguration.backgroundSession('com.example.bgdownload'),
onFinishedDownloading: (s, t, fileUri) {
actualContent = File.fromUri(fileUri).readAsStringSync();
});
final task = session.downloadTaskWithRequest(
URLRequest.fromUrl(Uri.https(...))
..resume();
멀티 플랫폼 앱에서의 플랫폼별 네트워킹
이 기능을 설계하면서 우리의 목표는 앱을 최대한 멀티 플랫폼으로 유지하는 것이었어요. 그 목표를 위해 기본 http 작업에는 일반적인 멀티 플랫폼 http API 세트를 유지하고, 플랫폼별로 어떤 네트워킹 라이브러리를 쓸지 설정할 수 있게 했어요. package:http Client API를 쓰면 작성해야 할 플랫폼별 코드를 최소화할 수 있어요. 이 API는 플랫폼별로 설정할 수 있지만 플랫폼과 무관하게 사용되거든요.
Dart 2.18은 package:http Client API를 지원하는 플랫폼별 http 라이브러리 두 개를 실험적으로 지원해요.
- macOS/iOS용
NSURLSession기반의cupertino_http. - Android에서 널리 쓰이는 네트워킹 라이브러리인 Cronet 기반의
cronet_http.
공통 클라이언트 API와 여러 HTTP 구현을 결합하면 둘 다의 장점을 얻을 수 있어요. 모든 플랫폼에 대해 단일 공유 소스 세트로 앱을 유지하면서도 플랫폼별 동작을 얻을 수 있는 거죠. 이 GitHub 이슈에 피드백을 남겨 주시면 감사하겠어요.
개선된 타입 추론
Dart는 제네릭 함수를 많이 사용해요. 요소 컬렉션을 단일 값으로 줄이는 fold 메서드를 생각해 볼게요. 아래 예시는 정수 리스트의 합을 계산해요.
List<int> numbers = [1, 2, 3];
final sum = numbers.fold(0, (x, y) => x + y);
print('The sum of $numbers is $sum');
Dart 2.17 이하에서는 이 메서드가 타입 오류를 반환해요.
line 2 • The operator '+' can't be unconditionally invoked
because the receiver can be 'null'.
Dart의 타입 추론은 인자 사이에서 정보를 흘려보낼 수 없었어요. 그 결과 x의 타입이 불확실해졌죠. 잠재적인 오류를 해결하려면 타입을 직접 지정해야 했어요.
final sum = numbers.fold(0, (int x, int y) => x + y);
Dart 2.18은 타입 추론을 개선했어요. 앞의 예시는 이제 정적 분석을 통과하고, x와 y 둘 다 non-nullable int라고 추론할 수 있어요. 이 변경으로 강하게 추론된 타입의 완전한 건전성(soundness) 속성을 유지하면서 더 간결한 Dart 코드를 작성할 수 있어요.
Async 성능 개선
이번 Dart 버전은 Dart VM이 async 메서드와 async*/sync* 제너레이터 함수를 적용하는 방식을 개선해요. 이로 인해 코드 크기가 줄었어요. 구글의 큰 내부 앱 두 개에서는 AOT 스냅샷 크기가 약 10% 줄어드는 것을 확인했어요. 마이크로벤치마크 전반에서도 성능 향상을 확인했고요.
이 변경에는 추가로 작은 동작 변경도 포함돼 있어요. 더 자세한 내용은 changelog를 참고해 주세요.
pub.dev 개선
2.18 릴리스와 함께 pub.dev 패키지 저장소에서 두 가지 변경을 했어요.
개인들이 pub.dev에 게시된 패키지를 여가 시간에 유지보수하는 경우가 많아요. 이건 시간과 금전 양쪽 모두에서 비용이 들 수 있죠. 후원을 돕기 위해 우리는 이제 pubspec에 새 funding 태그를 지원해요. 패키지 게시자가 패키지를 후원할 수 있는 한 가지 이상의 방법 링크를 나열하는 데 쓸 수 있죠. 그러면 이 링크들이 pub.dev의 사이드바에 표시돼요.
자세한 내용은 pubspec 문서를 참고해 주세요.
또한 우리는 풍부한 오픈소스 패키지 생태계를 장려하고 싶어요. 이를 강조하기 위해 pub.dev의 자동 패키지 점수는 OSI 승인 라이선스를 쓰는 패키지에 추가 10점을 부여해요.
몇 가지 브레이킹 체인지
Dart는 단순함과 배우기 쉬움에 강한 초점을 두고 있어요. 새 기능을 추가할 때도 신중한 균형을 유지하려고 끊임없이 노력하고 있죠. 단순함을 유지하는 방법 중 하나는 쓰임새가 적거나 더 나은 대체물이 있는 역사적 기능과 API를 제거하는 거예요. Dart 2.18은 이 범주에 속하는 것들을 정리하는데, 몇 가지 작은 브레이킹 체인지를 포함해요.
- 우리는 2020년 10월에 통합
dartCLI 개발자 도구를 추가했어요. 2.18에서 그 전환을 마쳤어요. 이번 릴리스는 마지막 두 개의 deprecated 도구인dart2js(dart compile js사용)와dartanalyzer(dart analyze사용)를 제거해요. - 언어 버전 관리의 도입과 함께
pub는 새 해석 파일.dart_tool/package_config.json을 생성해요. 이전 파일.packages는 버전을 담을 수 없는 형식을 썼어요. 우리는.packages파일 사용을 중단했어요..packages파일이 있다면 삭제해도 돼요. Object를 확장하지 않는 클래스의 mixin은 쓸 수 없어요(브레이킹 체인지 #48167). 이 동작은 원래 의도된 적이 없었어요.dart:io의RedirectException에 있는uri프로퍼티가 nullable로 바뀌었어요(브레이킹 체인지 #49045).- SCREAMING_SNAKE 규약을 따르는
dart:io네트워킹 API의 상수들이 제거됐어요(브레이킹 체인지 #34218. 이전에 deprecated 처리됨). 대응하는 lowerCamelCase 상수를 사용하세요. - Dart VM은 더 이상 종료 시 초기 터미널 설정을 복원하지 않아요.
Stdin설정lineMode와echoMode를 바꾸는 프로그램은 이제 프로그램 종료 시 설정을 복원할 책임이 있어요(브레이킹 체인지 #45630).
널 세이프티 업데이트
2020년 11월의 베타 릴리스와 2021년 3월의 Dart 2.12 출시 이후 널 세이프티가 널리 사용되는 걸 보게 되어 정말 기뻐요.
첫째, pub.dev의 인기 패키지 거의 대부분의 앱 개발자가 널 세이프티로 이주했어요. 분석에 따르면 가장 많이 쓰이는 패키지 상위 250개 중 100%, 상위 1,000개 중 98%가 널 세이프티를 지원해요.
둘째, 대부분의 앱 개발자가 완전한 널 세이프티 이주가 완료된 코드베이스에서 작업해요. 이건 아주 중요해요. Dart의 완전한 사운드 널 세이프티는 모든 코드와 모든 의존성(전이적 포함)을 이주해야만 발동하거든요. 우리는 flutter run 명령의 텔레메트리로 이걸 추적하고 있어요.
아래 그래프는 flutter run의 비사운드(unsound) 널 세이프티 실행과 사운드 널 세이프티 실행을 보여줘요. 널 세이프티가 도입되기 전에는 둘 다 없었어요. 그다음 비사운드 널 세이프티가 빠르게 늘어났죠. 앱들이 널 세이프티로 이주하기 시작하면서 개발자들은 부분 이주를 했어요. 일부 부분은 여전히 이주해야 했죠. 시간이 지나면서 우리는 사운드 널 세이프티 세션이 매우 건강하게 늘어나는 걸 확인했어요. 지난달 말 기준으로 사운드 널 세이프티 세션이 비사운드 세션보다 네 배나 많았어요. 앞으로 몇 분기 안에 사운드 널 세이프티가 100%에 가까워지는 걸 보게 되길 기대해요!
널 세이프티 로드맵의 중요한 업데이트
비사운드와 사운드 널 세이프티를 둘 다 지원하는 것은 오버헤드와 복잡성을 더해요.
첫째, Dart 개발자는 두 모드를 모두 배우고 이해해야 해요. Dart 코드를 읽을 때마다 언어 버전을 확인해서 타입이 기본적으로 non-null(Dart 2.12 이상)인지 nullable(Dart 2.11 이하)인지 확인해야 하죠.
둘째, 컴파일러와 런타임에서 두 모드를 모두 지원하는 것은 새 기능을 지원하도록 Dart SDK를 진화시키는 속도를 늦춰요.
비사운드 널 세이프티의 오버헤드와 앞 절에서 언급한 매우 긍정적인 도입 수치를 바탕으로, 우리의 목표는 사운드 널 세이프티만 지원하는 것으로 전환하고 non-null 안전성과 비사운드 널 세이프티 모드를 중단하는 거예요. 우리는 임시로 이걸 2023년 중반까지 릴리스하는 것으로 계획해 뒀어요.
이 말은 Dart 2.11 이하 지원을 중단한다는 뜻이에요. SDK 제약 조건의 하한이 2.12 미만인 pubspec 파일은 Dart 3 이상에서 더 이상 해석되지 않아요. 언어 마커를 담고 있는 소스 코드에서 그 값이 2.12 미만(예: // @dart=2.9)으로 설정돼 있으면 실패해요.
이미 사운드 널 세이프티로 이주했다면, 여러분의 코드는 Dart 3에서 완전한 널 세이프티로 동작할 거예요. 아직 이주하지 않았다면 지금 이주하세요! 이 변경 사항에 대해 더 자세히 알고 싶다면 이 GitHub 이슈를 확인해 보세요.
요약
인터롭, 네트워킹, 타입 추론, pub.dev에 대한 새 지원이 오늘부터 사용 가능해요. 시작하려면 Dart 2.18 릴리스를 직접 다운로드하거나, 오늘의 Flutter 3.3 SDK 릴리스에 통합된 형태로 가져올 수 있어요.