Dart 2.13 발표 — 타입 별칭, 개선된 FFI, 공식 Docker 지원

Dart 2.13 발표 — 타입 별칭, 개선된 FFI, 공식 Docker 지원

Dart 2.13에는 언어 이슈 트래커에서 두 번째로 많이 요청된 기능인 *타입 별칭(type alias)*이 추가돼요. 개선된 Dart FFI와 더 나은 성능, 그리고 Dart용 공식 Docker 이미지도 함께 제공돼요. 이 글에서는 2.12에서 도입한 null safety의 채택 현황, 2.13의 새 기능, Docker와 Google Cloud의 Dart 백엔드 지원 소식까지 차례로 살펴볼게요.

출처: Announcing Dart 2.13

본문

By Kevin Moore & Michael Thomsen

오늘 우리는 Dart 2.13을 발표해요. 현재 두 번째로 많이 요청된 언어 기능인 타입 별칭을 담고 있어요. Dart 2.13에는 개선된 Dart FFI와 더 나은 성능, 그리고 Dart용 새 Docker 공식 이미지도 포함돼 있어요. 이 글은 2.12에서 도입된 null safety 기능에 대한 업데이트를 전하고, 2.13의 새 기능을 논의하며, Dart 백엔드를 위한 Docker와 Google Cloud 지원에 대한 흥미로운 소식과 앞으로의 릴리스에서 기대할 수 있는 변화를 미리 보여 줘요.

null safety 업데이트

우리는 3월에 Dart 2.12 릴리스에서 사운드 null safety를 출시했어요. null safety는 Dart의 최신 주요 생산성 기능으로, 찾아내기 어려운 버그 부류인 null 오류를 피하는 데 도움을 주기 위한 것이에요. 그 출시와 함께 우리는 패키지 게시자들이 pub.dev의 공유 패키지를 null safety로 마이그레이션하기 시작하도록 독려했어요.

null safety가 얼마나 빨리 채택됐는지 보며 정말 기뻤어요! 출시 몇 달 만에 pub.dev에서 가장 인기 있는 상위 500개 패키지 중 93%가 이미 null safety를 지원해요. 그렇게 빨리 작업을 완수하고 전체 생태계가 나아가도록 도와준 모든 패키지 개발자에게 진심으로 감사드려요!

null safety를 지원하는 패키지가 이렇게 많으니, 여러분의 앱도 마이그레이션을 시작해도 될 가능성이 커요. 첫 단계는 dart pub outdated로 앱의 의존성을 확인하는 것이에요. 자세한 내용은 null safety 마이그레이션 가이드를 참고하세요. 우리는 dart createflutter create 템플릿도 바꿔서, 이제 새 앱과 패키지에서 기본적으로 null safety를 켜도록 했어요.

타입 별칭 발표

타입 별칭은 2.13 언어의 새 기능이에요. 이것은 함수 타입의 타입 별칭만 만들 수 있었던 이전 지원을 확장한 것으로, 다른 타입에는 적용되지 못했던 지원을 넓힌 것이에요. 이 매우 요구가 높았던 기능은 언어 이슈 트래커에서 두 번째로 높은 평가를 받았어요.

타입 별칭을 쓰면 어떤 기존 타입에든 새 이름을 만들 수 있고, 그 이름은 원래 타입을 쓸 수 있는 곳 어디에서나 사용할 수 있어요. 실제로 새 타입을 정의하는 것이 아니라, 간단한 축약 별칭을 도입하는 거예요. 그 별칭은 타입 동등성 검사까지 통과해요.

typedef Integer = int;

void main() {
  print(int == Integer); // true
}

그럼 타입 별칭은 어디에 쓸 수 있을까요? 한 가지 흔한 용도는 타입에 더 짧거나 더 설명적인 이름을 주어 코드를 더 읽기 쉽고 유지보수하기 좋게 만드는 것이에요.

좋은 예시가 JSON을 다루는 경우예요(이 예시를 제공한 GitHub 사용자 Levi-Lesches께 감사드려요). 여기서 우리는 Json이라는 타입 별칭을 정의할 수 있는데, JSON 문서를 String 키에서 임의 값(타입 dynamic)으로의 매핑으로 설명해요. 그다음 fromJson 명명된 생성자와 json getter를 정의할 때 그 Json 타입 별칭을 쓸 수 있어요.

typedef Json = Map<String, dynamic>;

class User {
  final String name;
  final int age;

  User.fromJson(Json json) :
    name = json['name'],
    age = json['age'];

Json get json => {
    'name': name,
    'age': age,
  };
}

클래스를 이름 짓는 타입 별칭에서 생성자도 호출할 수 있어서, 다음 코드는 완전히 유효해요.

main() {
  var j = Json();
  j['name'] = 'Michael';
}

타입 별칭으로 복잡한 타입에 이름을 붙이면, 코드를 읽는 사람이 코드의 불변 조건(invariant)을 이해하기 쉬워져요. 예를 들어 다음 코드는 제네릭 타입 X의 키와 List<X> 타입의 값을 담는 맵을 설명하는 타입 별칭을 정의해요. 단일 타입 매개변수를 가진 이름을 타입에 부여함으로써, 맵의 규칙적인 구조가 코드를 읽는 사람에게 더 분명하게 드러나요.

typedef MapToList<X> = Map<X, List<X>>;
void main() {
  MapToList<int> m = {};
  m[7] = [7]; // OK
  m[8] = [2, 2, 2]; // OK
  for (var x in m.keys) {
    print('$x --> ${m[x]}');
  }
}
=>
7 --> [7]
8 --> [2, 2, 2]

일치하지 않는 타입을 쓰려고 하면 분석 오류가 나요.

m[42] = ['The', 'meaning', 'of', 'life'];

=>

The element type 'String' can't be assigned to the list type 'int'.

공개 라이브러리에서 클래스 이름을 바꿀 때도 타입 별칭을 쓸 수 있어요. 공개 라이브러리에 PoorlyNamedClass라는 기존 클래스가 있는데 BetterNamedClass로 이름을 바꾸고 싶은 상황을 상상해 보세요. 클래스를 그냥 이름만 바꾸면, API 고객들은 갑자기 컴파일 오류를 겪게 돼요. 타입 별칭을 쓰면 이름을 바꾼 뒤에도 이전 클래스 이름에 대한 새 타입 별칭을 정의하고, 이전 이름에 @Deprecated 어노테이션을 추가할 수 있어요. 그러면 PoorlyNamedClass의 사용은 경고를 일으키지만, 이전처럼 계속 컴파일·동작해서 사용자들이 코드를 업그레이드할 시간을 얻을 수 있어요.

BetterNamedClass를 구현하고 PoorlyNamedClass를 deprecated 처리하는 방법을 볼게요(mylibrary.dart라는 파일에서):

class BetterNamedClass {...}

@Deprecated('Use BetterNamedClass instead')
typedef PoorlyNamedClass = BetterNamedClass;

그리고 누군가 PoorlyNamedClass를 사용하려고 하면 이렇게 돼요.

import 'mylibrary.dart';

void main() {
  PoorlyNamedClass p;
}

=>

'PoorlyNamedClass' is deprecated and shouldn't be used. Use BetterNamedClass instead.

타입 별칭 기능은 Dart 2.13부터 사용할 수 있어요. 사용하려면 pubspec의 하한 Dart SDK 제약을 최소 2.13으로 설정하세요.

environment:
  sdk: ">=2.13.0 <3.0.0"

이 기능은 언어 버저닝 덕분에 하위 호환돼요. SDK 제약이 2.13 미만인 패키지는 2.13 패키지에 정의된 타입 별칭을 안전하게 참조할 수 있어요. 다만 2.13 이전 패키지는 자신의 타입 별칭을 정의할 수 없어요.

Dart 2.13 FFI 변경

C 코드를 호출하는 상호 운용 메커니즘인 Dart FFI에도 새 기능이 몇 가지 있어요.

첫째, FFI가 이제 인라인 배열을 가진 구조체를 지원해요(#35763). 이런 인라인 배열을 가진 C 구조체를 생각해 볼게요.

struct MyStruct {
  uint8_t arr[8];
}

이제 이를 Dart에서 직접 감싸고, Array에 타입 인자로 요소 타입을 지정할 수 있어요.

class StructInlineArray extends Struct {
  @Array(8)
  external Array<Uint8> arr;
}

둘째, FFI가 이제 패킹된 구조체(packed structs)를 지원해요(#38158). 일반적으로 구조체는 CPU가 접근하기 더 쉬운 주소 경계에 멤버가 놓이도록 메모리에 배치돼요. 패킹된 구조체에서는 이러한 패딩 중 일부를 생략해 전체 메모리 사용량을 낮추는데, 종종 플랫폼별 방식으로 그렇게 해요. 새 @Packed(<alignment>) 어노테이션으로 패딩을 쉽게 지정할 수 있어요. 예를 들어 다음 코드는 메모리에서 4바이트 정렬을 갖는 구조체를 만들어요.

@Packed(4)
class TASKDIALOGCONFIG extends Struct {
  @Uint32()
  external int cbSize;
  @IntPtr()
  external int hwndParent;
  @IntPtr()
  external int hInstance;
  @Uint32()
  external int dwFlags;
  ...
}

Dart 2.13 성능 변경

우리는 Dart 코드의 앱 크기와 메모리 사용량을 줄이는 작업을 계속해요. 큰 Flutter 애플리케이션에서, AOT 컴파일된 Dart 프로그램의 메타데이터를 나타내는 내부 구조가 상당한 메모리 덩어리를 차지할 수 있어요. 이 메타데이터의 대부분은 hot reload, 대화형 디버깅, 사람이 읽을 수 있는 스택 트레이스 포맷팅 같은 기능을 지원하기 위해 존재해요. 이런 기능들은 배포된 애플리케이션에서는 결코 사용되지 않아요. 지난 1년 동안 우리는 이 오버헤드를 최대한 없애기 위해 Dart 네이티브 런타임을 재구성해 왔어요. 일부 개선은 릴리스 모드로 빌드된 모든 Flutter 애플리케이션에 적용되지만, 어떤 것들은 --split-debug-info 플래그로 AOT 컴파일된 애플리케이션에서 디버그 정보를 분리해서, 사람이 읽을 수 있는 스택 트레이스를 포기해야 적용돼요.

Dart 2.13은 --split-debug-info를 사용할 때 프로그램 메타데이터가 차지하는 공간을 크게 줄여 주는 여러 변경을 포함해요. Flutter Gallery 앱을 예로 들어 볼게요. Android에서 릴리스 APK는 디버그 정보 포함 시 112.4MB, 미포함 시 106.7MB예요(전체 5% 감소). 이 APK에는 자산이 많이 들어 있어요. APK 안의 코드 메타데이터만 보면, Dart 2.12에서는 5.7MB였던 것이 Dart 2.13에서는 3.7MB로 줄었어요(35% 감소).

앱 크기와 메모리 사용량이 중요하다면, --split-debug-info 플래그를 사용해 디버그 정보를 생략하는 것을 고려해 보세요. 그렇게 할 때는 symbolize 명령을 사용해 스택 트레이스를 다시 사람이 읽을 수 있게 해야 한다는 점을 참고하세요.

공식 Docker 지원과 Google Cloud에서의 Dart

Dart가 이제 Docker 공식 이미지로 제공돼요. Dart는 수년간 Docker 이미지를 제공해 왔지만, 이 새 Dart 이미지는 Docker가 모범 사례를 따르도록 테스트하고 검증했어요. 또 AOT(사전 컴파일) 컴파일을 지원해서, Cloud Run 같은 컨테이너 환경에서 빌드된 컨테이너 크기를 극적으로 줄이고 배포 속도를 높일 수 있어요.

Dart가 Flutter 같은 앱 프레임워크가 모든 화면에서 아름다운 픽셀을 구동하도록 돕는 데 계속 초점을 두는 한편, 우리는 대부분의 사용자 경험 뒤에 최소한 하나의 호스팅된 서비스가 있다는 걸 알고 있어요. Dart로 백엔드 서비스를 쉽게 만들 수 있게 함으로써, 개발자가 프런트엔드의 위젯을 구동하는 것과 같은 언어와 비즈니스 로직으로 애플리케이션을 클라우드로 확장할 수 있는 전체 스택 경험을 지원해요.

일반적으로 Flutter 앱 백엔드에 Dart를 쓰는 것은 Google의 관리형 serverless 플랫폼인 Cloud Run의 단순함과 확장성에 특히 잘 맞아요. 여기에는 scale-to-zero가 포함되어, 백엔드가 어떤 요청도 처리하지 않을 때 비용이 발생하지 않아요. 우리는 Google Cloud 팀과 협력하여, HTTP 요청과 CloudEvents를 처리하기 위해 전체 서버 대신 배포할 Dart 함수를 쉽게 작성할 수 있게 해 주는 패키지·도구·예시 모음인 Functions Framework for Dart를 제공해요.

시작하려면 Google Cloud 문서를 확인해 보세요.

앞으로 무엇이 있을지에 대한 몇 마디

우리는 다가올 릴리스를 위한 흥미로운 변화를 이미 만들고 있어요. 항상 그렇듯 언어 깔때기 트래커로 우리의 진행 상황을 지켜볼 수 있어요.

우리가 작업 중인 한 영역은 Dart와 Flutter 둘 다를 위한 새 표준(canonical) lint 집합이에요. Lint는 Dart 정적 분석을 구성하는 강력한 방법이지만, 켜고 끌 수 있는 lint가 수백 개나 되다 보니 무엇을 선택할지 정하기 어려울 수 있어요. 우리는 Dart와 Flutter 프로젝트에서 기본으로 적용할 두 표준 lint 집합을 정의하는 데 현재 작업하고 있어요. 다음 안정 릴리스에서 기본으로 활성화될 것으로 기대해요. 미리 보고 싶다면 lintsflutter_lints 두 패키지를 확인해 보세요.

마지막으로, Dart VM 런타임을 깊이 임베딩한다면, 기존 메커니즘을 deprecated 처리할 계획이라는 점을 알아두세요. 우리는 그것을 Dart FFI에 기반한 더 빠르고 더 유연한 모델로 대체할 거예요(추적 이슈 #45451 참고).

Dart 2.13은 지금 사용 가능해요

타입 별칭과 개선된 FFI를 담은 Dart 2.13은 오늘 Dart 2.13Flutter 2.2 SDK에서 사용할 수 있어요.

의존성이 null safety로 마이그레이션되기를 기다려 왔다면, dart pub outdated로 다시 확인해 볼 만해요. 인기 상위 500개 패키지의 93%가 이미 마이그레이션됐으니, 막힘없이 진행될 가능성이 커요. 이미 마이그레이션한 개발자분들께도 큰 감사를 전해요!

이 블로그 포스트에서 논의한 새 기능과 변경에 대한 여러분의 경험을 듣고 싶어요. 아래에 댓글을 남기거나 @dart_lang으로 트윗해 주세요.

더 알아보기