Dart 2.5 발표: 개발 경험을 한층 끌어올리다

Dart 2.5 발표: 개발 경험을 한층 끌어올리다

오늘 Dart 2.5 SDK의 안정 버전을 발표해요. 이번 버전에는 두 가지 주요한 새 개발자 기능의 기술 프리뷰가 포함되어 있어요. 하나는 머신러닝(ML) 기반 코드 완성인 ML Complete, 다른 하나는 Dart에서 C 코드를 직접 호출하기 위한 외부 함수 인터페이스 **dart:ffi**예요. Dart 2.5에는 상수 표현식(constant expression) 지원도 개선됐어요.

이번 릴리스는 "어떤 플랫폼에서든 빠른 앱을 만들기 위한 최고의 클라이언트 최적화 언어"라는 우리의 비전을 향한 또 한 걸음이에요. ML Complete는 핫 리로드(hot reload), 커스터마이즈 가능한 정적 분석, Dart DevTools 같은 기존 생산성 도구 모음에 더해지는 강력한 추가 기능이에요. 두 번째 프리뷰 기능인 dart:ffi는 Dart 코드가 실행되는 여러 운영체제에서 기존 네이티브 API와, C로 작성된 기존 크로스플랫폼 네이티브 라이브러리를 활용할 수 있게 해줘요.

최고의 클라이언트 최적화 언어를 만들겠다는 우리의 야망에 대해 말하자면, 지난주에 나온 새 IEEE Spectrum Top Programming Language 2019 순위에 Dart가 포함된 것도 정말 반가웠어요. Dart는 IEEE Spectrum 프로그래밍 언어 순위에서 16위로 진입했어요. 트렌딩 기준으로는 10위, 모바일 언어로 필터링하면 6위(Java, C, C++, C#, Swift 뒤)예요.

출처: Announcing Dart 2.5: Super-charged development

본문

프리뷰: ML Complete — 머신러닝으로 순위 매겨지는 코드 완성

타입이 있는 프로그래밍 언어의 핵심 장점 중 하나는, 타입에 담긴 추가 정보 덕분에 IDE/에디터가 개발자가 코드를 입력하는 동안 완성(completion)을 제공해 도움을 줄 수 있다는 거예요. 코드 완성 덕분에 개발자는 오타를 피할 수도 있고, 예상되는 심볼의 시작 부분을 입력하고 제안된 완성 중에서 고르면서 API를 탐색할 수도 있죠.

그런데 API가 커질수록 탐색이 어려워져요. 가능한 완성 목록이 너무 길어져서 알파벳순으로 훑어보기엔 버거워지거든요. 지난 1년 동안 우리는 이 문제에 머신러닝을 적용하는 데 열심히 작업했어요. 간단히 말하면, 대규모 GitHub 오픈소스 Dart 코드 코퍼스를 분석해서 주어진 컨텍스트에 기반한 "그럴듯한 멤버 출현" 모델을 훈련시키는 방식이에요. 이 모델은 TensorFlow Lite로 구동되는데, 개발자가 편집 중일 때 다음에 올 심볼을 예측하는 데 사용돼요. 우리는 이 새 기능을 ML Complete라고 불러요. 아래는 Flutter 프레임워크로 새 MyHome 위젯을 개발하는 예시예요.

이게 어떻게 동작하는지 좀 더 자세히 볼게요. 지금 시각으로부터 하루 뒤의 시각을 계산하는 작은 프로그램을 쓴다고 상상해 보세요. ML Complete를 쓰면 아래 왼쪽 같은 경험을 하게 돼요. 먼저 now라는 변수 이름에 기반해서 DateTime.now() 완성을 제안할 수 있다는 점을 주목하세요. 다음으로 tomorrow 변수 이름에서 완성이 얻어지고, 마지막으로 add(…)now에 대해 두 번째로 제안되는 완성이라는 점을 보세요. 오른쪽의 ML Complete가 없는 경험에서는 DateTime을 수동으로 입력해야 하고, tomorrow 변수 이름에서는 완성이 전혀 나오지 않으며, add(…) 메서드는 목록에서 훨씬 아래에 있어요.

ML Complete 사용해 보기

ML Complete는 오늘 프리뷰로 출시해요. Dart 분석기(analyzer)에 직접 내장되어 있어서 Android Studio, IntelliJ, VS Code를 포함한 모든 Dart 지원 에디터에서 사용할 수 있어요. 이 프리뷰 기능을 켜는 방법과 피드백 제공·이슈 보고 방법에 대한 자세한 내용은 이 위키 페이지를 참고하세요.

이 기능은 아직 프리뷰라서, 현재 Flutter와 Dart 안정 릴리스에 포함된 버전에는 이후 빌드에서 기대하는 성능과 완성도 작업이 들어 있지 않아요. 그래서 이 기능을 프리뷰할 때는 Flutter dev 채널이나 Dart dev 채널을 임시로 사용하는 걸 권장해요.

프리뷰: Dart-C 인터럽을 위한 dart:ffi 외부 함수 인터페이스

많은 개발자가 Dart에서 C 코드를 호출하는 더 나은 지원을 요청해 왔어요. 아주 분명한 신호 하나는 Flutter 이슈 트래커예요. C 인터럽(interop)이 600개가 넘는 👍 투표를 받은 단일 오픈 기능 요청 중 가장 높은 순위를 차지하고 있거든요. 이 요청들 뒤에는 흥미로운 사용 사례가 많아요. stdlib.h나 Win32 같은 저수준 플랫폼 API를 호출하는 것부터, TensorFlow, Realm, SQLite처럼 C로 작성된 기존 크로스플랫폼 라이브러리와 유틸리티를 활용하는 것까지 다양해요.

현재 Dart에서 C를 직접 호출하는 지원은 네이티브 확장(native extensions)을 사용해 Dart VM에 깊게 통합되는 경우로 제한돼 있어요. 또는 Flutter 앱은 플랫폼 채널(platform channels)로 호스트를 호출하고 거기서부터 C를 호출하는 방식으로 C를 간접적으로 쓸 수 있는데, 이건 바람직하지 않은 이중 간접(indirection)이에요. 우리는 훌륭한 성능을 제공하고 접근하기 쉬우며, 지원되는 여러 Dart 플랫폼과 컴파일러에서 동작하는 새 메커니즘을 제공하려고 해요.

Dart-C 인터럽은 두 가지 주요 시나리오를 가능하게 해요.

  • 호스트 운영체제(OS)의 C 기반 시스템 API 호출
  • 단일 OS용이거나 크로스플랫폼인 C 기반 라이브러리 호출

C 기반 운영체제 API 호출하기

첫 번째 범주를 구체적으로 살펴볼게요. Linux의 system 명령을 호출해 볼게요. 이 명령은 어떤 시스템 명령이든 실행하게 해줘요. 여기에 넘기는 인자는 본질적으로 셸/터미널에 전달되어 거기서 실행돼요. 이 명령의 C 헤더는 다음과 같아요.

// C header: int system(const char *command) in stdlib.h

어떤 인터럽 메커니즘이든 핵심 과제는 두 언어 간의 의미(semantics) 차이를 다루는 거예요. dart:ffi의 경우 Dart 코드가 두 가지를 나타내야 해요.

  • C 함수와 그 인자·반환 타입
  • 대응하는 Dart 함수와 그 타입

우리는 두 개의 typedef를 정의해서 그렇게 해요.

// C header typedef:
typedef SystemC = ffi.Int32 Function(ffi.Pointer<Utf8> command);

// Dart header typedef:
typedef SystemDart = int Function(ffi.Pointer<Utf8> command);

다음으로 라이브러리를 로드하고 호출할 함수를 찾아야 해요. 이걸 어떻게 하는지는 운영체제에 따라 달라요. 이 예시에서는 macOS를 사용해요. (macOS, Windows, Linux용 완전한 예시가 준비되어 있어요.)

// Load `stdlib`. On MacOS this is in libSystem.dylib.
final dylib = ffi.DynamicLibrary.open('/usr/lib/libSystem.dylib');

// Look up the system function.
final systemP = dylib.lookupFunction<SystemC, SystemDart>('system');

다음으로 문자열 인자를 특정 운영체제에 맞는 인코딩으로 인코딩하고, 함수를 호출한 뒤 인자 메모리를 다시 해제해요.

// Allocate a pointer to a Utf8 array containing our command.
final cmdP = Utf8.toUtf8('open http://dart.dev');

// Invoke the command.
systemP(cmdP);

// Free the pointer.
cmdP.free();

이 코드는 system 명령을 실행해서, 시스템 기본 브라우저가 dart.dev를 열게 해요.

C 기반 프레임워크와 컴포넌트 호출하기

dart:ffi의 두 번째 핵심 용도는 C 기반 프레임워크와 컴포넌트를 호출하는 거예요. 이 글 서두에서 다룬 ML 기반 코드 완성이 바로 그 구체적인 예시예요! 이 기능은 C 기반 API인 TensorFlow Lite를 사용해요. dart:ffi를 사용하면 코드 완성을 제공해야 하는 모든 운영체제에서, 네이티브 TensorFlow 구현의 높은 성능으로 TensorFlow를 실행할 수 있어요. Dart TensorFlow 통합 코드를 살펴보고 싶다면 이 저장소를 확인해 보세요.

또한 C 기반 라이브러리를 호출할 수 있는 능력이 Flutter 앱에도 큰 도움이 될 거라고 기대해요. Realm이나 SQLite 같은 네이티브 라이브러리를 호출하는 상황을 상상해 볼 수 있고, dart:ffi가 Flutter 데스크톱용 플러그인을 가능하게 하는 데 유용할 거라 생각해요.

API 감싸기와 코드 생성

눈치채셨겠지만, 함수를 기술하고 그 심볼을 찾아내는 데 약간의 프로그래밍 오버헤드가 있어요. 이런 상용구 코드 중 상당수는 C 헤더 파일로부터 생성될 수 있어요. 우리는 현재 기반 프리미티브를 제공하는 데 집중하고 있지만, 생성기(generator) 작업에 관심 있는 누구와도 협업하고 싶어요.

dart:ffi 사용해 보기

dart:ffi 라이브러리는 오늘 프리뷰로 출시돼요. 아직 프리뷰라서, 우리의 변경 사항과 개선 사항이 만들어지는 대로 빠르게 접근하려면 Flutter master 채널이나 Dart dev 채널을 사용하길 권장해요. 다듬어지고 일반적인 패턴에 대한 지원이 넓어짐에 따라, API는 완성되기 전까지 breaking change가 있을 가능성이 높다는 점을 참고하세요. 첫 릴리스에 대해 우리가 현재 계획하고 있는 내용을 자세히 볼 수 있어요. 알아 두면 좋은 몇 가지 제약이 있어요.

  • 라이브러리는 중첩 구조체(nested structs), 인라인 배열(inline arrays), 패킹된 데이터(packed data), 플랫폼 종속 프리미티브 타입을 지원하지 않아요.
  • 포인터 연산의 성능이 부족해요(Pointer.asExternalTypedData로 우회할 수 있어요).
  • 라이브러리는 finalizer를 지원하지 않아요(객체가 가비지 컬렉션되려 할 때 호출되는 콜백).

C 인터럽 문서와 dart:ffi API 레퍼런스에서 핵심 개념을 설명하고 검토할 수 있는 예시를 안내해요. 문제가 있거나 질문이 있다면 Dart FFI 토론 그룹에 올리거나 이슈를 등록해 주세요.

개선된 상수 표현식

Dart는 오랫동안 const 변수와 값을 만드는 걸 지원해 왔어요. 이들은 컴파일 타임에 상수임이 보장되어서 매우 좋은 성능 특성을 가져요. 이전 릴리스에서는 상수 표현식이 조금 제한적이었는데, Dart 2.5부터는 상수 표현식을 정의하는 훨씬 더 많은 방법을 지원해요. Dart 2.3에서 출시된 cast와 새로운 제어 흐름·컬렉션 스프레드 기능까지 사용할 수 있어요.

// Example: these are now valid compile-time constants.
const Object i = 3;
const list = [i as int];
const set = {if (list is List<int>) ...list};
const map = {if (i is int) i: "int"};

마무리하며

앞으로 몇 분기 동안 들뜨는 일정이 잡혀 있어요. 확장 메서드(extension methods) 작업이 한창 진행 중이고, 참조가 non-nullable임을 강제하는 작업과, 그 너머의 언어에 대한 초기 계획도 진행 중이에요. 또 개선된 동시성 지원 — 예를 들어 최신 모바일 폰의 멀티코어 프로세서를 더 잘 활용하는 능력 — 도 조사하고 있어요.

우리는 특히 non-nullable-by-default에 열정적이에요. 이 기능에 대해 꽤 야심 찬 계획을 세워 두었고, 많은 작업이 진행 중이에요. 최근 몇몇 언어는 처음부터 non-nullable을 지원하도록 설계됐지만, 대부분의 기존 언어는 나중에 non-nullable을 추가하면서 추가 정적 분석에 국한된 상당히 제한적인 접근에 머물렀어요. 우리 접근의 주요 차별점 중 하나는 완전한 사운드(sound) non-nullable 지원을 목표로 한다는 거예요. 간단히 설명하면, non-nullability에 대한 우리의 이해가 타입 시스템의 핵심까지 확장되고, 우리 타입 시스템이 어떤 것이 non-nullable임을 알게 되면 그 정보를 완전히 신뢰하며, 백엔드 컴파일러가 자유롭게 코드를 최적화할 수 있다는 뜻이에요. 이 사운드함은 일관된 "예외 없는 경험"을 제공한다는 점과 코드 크기·런타임 성능 측면에서 큰 이점이 있어요.

우리는 언어를 바꿀 때마다 생태계에 부담이 된다는 걸 항상 알고 있어요. 그래서 기존 코드를 위한 풍부한 마이그레이션 도구를 제공하는 데도 많은 투자를 하고 있어요. 이 도구가 마이그레이션 비용의 대부분을 상쇄하길 바래요. 단계별 마이그레이션을 가능하게 하는 전용 언어·도구 기능도 몇 가지 추가하고 있고, 우리 자신의 코드와 공유 코드를 https://pub.dev로 마이그레이션하는 데도 노력을 기울일 거예요.

올해 후반에 더 많은 소식을 공유할 수 있길 기대해요!

더 알아보기