Dart 3 출시 소식: 100% 사운드 null safety, records, patterns, class modifiers

Dart 3 출시 소식: 100% 사운드 null safety, records, patterns, class modifiers

Google I/O 2023에서 보내는 인사예요. 오늘 마운틴뷰에서 라이브로, 지금까지 가장 큰 Dart 릴리스인 Dart 3를 발표해요! Dart 3에는 세 가지 주요 발전이 담겨 있어요. 첫째, 100% 사운드 null safety로의 여정을 완료했어요. 둘째, records, patterns, class modifiers라는 주요 새 언어 기능을 추가했어요. 셋째, Wasm 컴파일을 통한 웹 네이티브 코드 지원으로 플랫폼 지원을 넓히는 미래를 미리 보여드려요. 자세한 내용을 함께 파고들어 볼게요.

출처: Announcing Dart 3

본문

100% sound null safety

지난 4년 동안 우리는 Dart를 빠르고, 휴대 가능하며, 현대적인 언어로 발전시켜 왔어요. 이제 Dart 3에서 그것은 100% sound null safe 언어가 됐어요! 이전에 논의했듯이, 어떤 프로그래밍 언어도 기존 언어에 sound null safety를 추가한 적이 없다고 생각해요. 그래서 꽤나 긴 여정이었죠.

Dart에서 null safety가 100% 구현되면, 우리는 sound(건전한) 타입 시스템을 갖게 돼요. 어떤 타입이 값이 null이 아니라고 말한다면, 그것이 절대 null이 될 수 없다고 신뢰할 수 있어요. 이는 null 포인터 예외 같은 특정 종류의 코딩 오류를 피하게 해줘요. 또한 컴파일러와 런타임이 null safety가 없을 때는 불가능했던 방식으로 코드를 최적화할 수 있게 해줘요. 이 설계 선택에는 트레이드오프가 따랐어요. 마이그레이션이 조금 더 어려워졌지만, 우리는 Dart를 위해 올바른 선택을 했다고 믿어요.

Dart 3으로 마이그레이션하기

sound null safety를 달성하는 데 있어 중요한 부분은 Dart 커뮤니티의 흔들림 없는 지원이었어요. pub.dev에서 상위 1000개 패키지의 99%가 null safety를 지원해요!

이를 감안하면, null safety로 마이그레이션된 대부분의 패키지와 앱이 Dart 3에서 동작할 것이라고 기대해요. 아주 일부 경우에만 Dart 3의 관련 정리가 코드에 약간의 영향을 줄 수 있어요. 일부 레거시 코어 라이브러리 API가 제거됐고(#34233, #49529), 일부 도구가 조정됐어요(#50707). Dart 3 SDK 사용으로 마이그레이션하면서 문제가 생기면 Dart 3 마이그레이션 가이드를 참고해 주세요. 그 외에는 합리화된(reasoned) 새 코어 라이브러리와 도구를 즐기시길 바래요.

주요 언어 기능 — Records, patterns, class modifiers

Dart 3은 기존 언어를 바꾸는 데 그치지 않아요. records, patterns, class modifiers 같은 의미 있는 새 기능과 능력도 추가해요!

records로 구조화된 데이터 만들기

전통적으로 Dart 함수는 단일 값만 반환할 수 있었어요. 그래서 여러 값을 반환해야 하는 함수는 그 값들을 map이나 list 같은 다른 데이터 타입에 담거나, 값을 보관할 새 클래스를 정의해야 했죠. 타입 없는 데이터 구조를 쓰는 것은 타입 안전성을 약화시켰어요. 데이터를 담기 위해 새 클래스를 정의하는 것은 코딩 과정에 마찰을 더했어요. 여러분이 이걸 분명히 알려줬어요. 여러 반환 값에 대한 언어 요청이 우리의 네 번째로 높은 평가를 받은 이슈예요.

records를 쓰면 깔끔하고 명확한 문법으로 구조화된 데이터를 만들 수 있어요. 이 함수를 볼게요. JSON blob에서 이름과 나이를 읽어 둘 다 record로 반환해요.

(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['height'] as int);
}

이건 모든 Dart 개발자에게 익숙해 보일 거예요. record는 ['Michael', 'Product Manager'] 같은 리스트 리터럴처럼 보이지만, 대괄호 대신 괄호를 써요. Dart에서 records는 일반적인(general) 기능이에요. 함수 반환 값 이상으로 쓰일 수 있어요. 변수에 저장하고, 리스트에 넣고, map의 키로 쓰고, record를 포함한 record를 만들 수도 있어요. 앞선 예시처럼 이름 없는 필드(unnamed fields)를 추가할 수도 있고, (42, description: 'Meaning of life')처럼 이름 있는 필드(named fields)를 추가할 수도 있어요.

records는 값 타입(value type)이라 정체성(identity)이 없어요. 이 덕분에 컴파일러가 어떤 경우에는 record 객체를 완전히 지워버릴 수도 있어요. records는 또한 자동으로 정의된 == 연산자와 hashCode 함수를 갖춰요. 자세한 내용은 records 문서에서 볼 수 있어요.

patterns와 pattern matching으로 구조화된 데이터 다루기

records는 구조화된 데이터를 만드는 걸 단순화해요. 하지만 이게 더 공식적인 타입 계층을 만드는 클래스를 대체하진 않아요. 그냥 하나의 선택지를 더 제공하는 거예요. 어느 쪽이든, 그 구조화된 데이터를 개별 요소로 쪼개서 다루고 싶을 수 있어요. 이때 pattern matching이 등장해요.

pattern의 기본 형태를 살펴볼게요. 다음 record pattern은 record를 두 개의 새 변수 nameheight로 디스트럭처링해요. 이 변수들은 print 호출처럼 다른 변수와 똑같이 쓸 수 있어요.

var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
print('User $name is $height cm tall.');

list와 map에도 비슷한 pattern이 있어요. 이 모든 것에서 밑줄 pattern(underscore pattern)으로 개별 요소를 건너뛸 수 있어요.

var (String name, _) = userInfo(…);

pattern은 switch 문에서 쓸 때 빛을 발해요. Dart는 처음부터 switch 지원이 제한적이었어요. Dart 3에서는 switch 문의 힘과 표현력을 넓혔어요. 이제 이런 경우에 pattern matching을 지원해요. 각 case 끝에 break를 추가할 필요도 없어졌고, 논리 연산자로 case를 결합하는 것도 지원해요. 다음 예시는 문자 코드를 파싱하는 깔끔하고 명확한 switch 문을 보여줘요.

switch (charCode) {
  case slash when nextCharCode == slash:
    skipComment();

  case slash || star || plus || minus:
    operator(charCode);

  case >= digit0 && <= digit9:
    number();

  default:
    invalid();
}

switch 문은 각 case에 문(statement)이 하나 이상 필요할 때 큰 도움이 돼요. 어떤 경우에는 값만 계산하고 싶을 뿐이에요. 그런 사용 사례를 위해 매우 간결한 switch *표현식(expression)*을 제공해요. 이것은 switch 과 비슷하지만, 표현식에 맞게 미세 조정된 다른 문법을 써요. 다음 샘플 함수는 오늘의 요일 설명을 계산하기 위해 switch 표현식의 값을 반환해요.

String describeDate(DateTime dt) =>
  switch (dt.weekday) {
      1 => 'Feeling the Monday blues?',
      6 || 7 => 'Enjoy the weekend!',
      _ => 'Hang in there.'
  };

pattern의 강력한 기능 중 하나는 "exhaustiveness"(누락 없음)를 검사하는 능력이에요. 이 기능은 switch가 가능한 모든 case를 처리하는지 보장해요. 앞선 예시에서 우리는 int인 weekday의 모든 가능한 값을 처리하고 있어요. 특정 값 1, 6, 7에 대한 매치 문과 나머지 case를 위한 기본 case _를 조합해 모든 가능한 값을 빠짐없이 처리해요. 클래스 계층 같은 사용자 정의 데이터 계층에 대해 이 검사를 활성화하려면, 다음 예시처럼 클래스 계층의 최상단에 새 sealed 수정자를 사용해요.

sealed class Animal { … }
class Cow extends Animal { … }
class Sheep extends Animal { … }
class Pig extends Animal { … }

String whatDoesItSay(Animal a) =>
    switch (a) { Cow c => '$c says moo', Sheep s => '$s says baa' };

이것은 마지막 가능한 하위 타입인 Pig를 처리하지 않았다는 다음 오류를 반환해요.

line 6 • The type 'Animal' is not exhaustively matched by the switch cases
since it doesn't match 'Pig()'.

마지막으로, if 문도 pattern을 쓸 수 있어요. 다음 예시에서는 if-case 매칭을 map-pattern에 적용해 JSON map을 디스트럭처링해요. 그 안에서 상수 값('name', 'Michael' 같은 문자열)과 타입 테스트 pattern int h에 매칭해 JSON 값을 읽어요. pattern 매칭이 실패하면 Dart는 else 문을 실행해요.

final json = {'name': 'Michael', 'height': 180};

// Find Michael's height.
if (json case {'name': 'Michael', 'height': int h}) {
  print('Michael is $h cm tall.');
} else {
  print('Error: json contains no height info for Michael!');
}

이건 pattern으로 할 수 있는 일의 일부에 불과해요. 우리는 pattern이 모든 Dart 코드에 널리 퍼지게 될 거라고 믿어요. 더 자세히 알고 싶다면 patterns 문서patterns codelab을 확인해 보세요.

class modifiers로 클래스에 세밀한 접근 제어

세 번째 Dart 3 언어 기능은 class modifiers예요. 모든 Dart 개발자가 쓰게 될 것이라 기대하는 records와 patterns와 달리, 이건 더 파워 유저를 위한 기능이에요. 큰 API 표면을 만들거나 엔터프라이즈급 앱을 구축하는 Dart 개발자의 요구를 다루는 기능이죠.

class modifiers를 쓰면 API 작성자가 특정 능력 집합만 지원할 수 있어요. 하지만 기본값은 변하지 않아요. Dart를 단순하고 접근하기 쉽게 유지하고 싶으니까요. 그래서 이전처럼 일반 클래스는 다음 예시처럼 *구성(construct)*되고, *확장(extend)*되고, *구현(implement)*될 수 있어요.

class Vehicle {
  String make; String model;
  void moveForward(int meters) { … }
}

// Construct.
var myCar = Vehicle(make: 'Ford', model: 'T',);

// Extend.
class Car extends Vehicle {
  int passengers;
}

// Implement.
class MockVehicle implements Vehicle {
  @override void moveForward …
}

class modifiers는 여기에 제한을 추가하는 걸 지원해요. 몇 가지 사용 사례를 볼게요.

  • interface class를 쓰면 다른 쪽에서 구현할 계약(contract)을 정의할 수 있어요. interface class는 확장할 수 없어요.
  • base class를 쓰면 클래스의 모든 하위 타입이 그 인터페이스를 구현하는 대신 클래스에서 상속받도록 보장할 수 있어요. 이렇게 하면 private 메서드가 모든 인스턴스에서 사용 가능하게 보장돼요.
  • final class로 타입 계층을 닫아, 자신의 라이브러리 밖에서 어떤 하위 클래스도 만들 수 없게 막을 수 있어요. 이점의 예로, API 소유자가 API 소비자에게 브레이킹 체인지 위험을 주지 않고 새 멤버를 추가할 수 있게 된다는 점이 있어요.

자세한 내용은 새 class modifiers 문서를 보세요.

미래를 향한 전망

Dart 3은 오늘 바로 쓸 수 있는 새 기능 측면에서 큰 전진일 뿐만 아니라, 다음에 올 것을 미리 보여드려요.

Dart 언어

records, patterns, class modifiers는 매우 큰 새 기능이라, 설계 중 일부가 개선될 여지가 있을 수 있어요. 우리는 여러분의 피드백을 계속 지켜보면서, Dart 3 이후의 마이너 릴리스에서 업데이트가 필요한지 살펴볼 거예요.

또한 완전히 논브레이킹이고 마이그레이션 비용 없이 개발자 생산성을 높이는 데 집중한, 더 작고 점진적인 기능도 살펴보고 있어요. 탐색 중인 두 예는, 기존 타입을 제로 코스트 "래퍼"로 감싸는 inline classes와, 필드 몇 개와 primary constructor를 가진 클래스를 정의하는 훨씬 간결한 문법을 도입하는 primary constructors예요.

우리는 이전에 macros(즉 메타프로그래밍)를 논의했어요. 특히 JSON(및 유사한 것)의 더 나은 역직렬화를 가능하게 하고, data classes를 가능하게 하는 데 집중하고 있어요. 메타프로그래밍의 규모와 내재된 위험을 고려해 우리는 매우 철저한 접근을 취하고 있고, 그래서 설계 결정을 마무리하는 것조차 아직 구체적인 타임라인을 공유할 수 없어요.

네이티브 인터롭

모바일과 데스크톱의 앱은 알림, 결제, 휴대폰 위치 가져오기 등, 네이티브 플랫폼이 제공하는 많은 API에 의존해요. 전통적으로 Flutter에서 이것들은 플러그인을 빌드해 접근하는데, API용 Dart 코드와 구현을 제공하는 다양한 플랫폼 특화 코드를 모두 작성해야 해요.

우리는 이미 dart:ffi로 C 라이브러리로 컴파일되는 코드와의 인터롭을 지원해요. 현재 이걸 Android에서의 Java와 Kotlin 인터롭, iOS/macOS에서의 Objective-C와 Swift 인터롭으로 확장하는 작업을 하고 있어요. Android 인터롭 소개는 새 Google I/O 23 Android interoperability 동영상을 확인해 보세요.

WebAssembly로의 컴파일 — 네이티브 코드로 웹을 겨냥하기

WebAssembly(줄여서 Wasm)는 모든 현대 브라우저에서 플랫폼 중립적인 바이너리 명령 형식으로 성숙해지고 있어요. Flutter 프레임워크는 한동안 Wasm을 사용해 왔어요. C++로 작성된 SKIA 그래픽 렌더링 엔진을 Wasm 컴파일 모듈을 통해 브라우저에 전달하는 방식이 그거예요. 우리는 오랫동안 Wasm을 사용해 Dart 코드도 배포하는 데 관심이 있었지만, 막혀 있었어요. Dart는 다른 많은 객체 지향 언어처럼 가비지 컬렉션을 사용하거든요. 지난 1년 동안 우리는 Wasm 생태계의 여러 팀과 협력해 WebAssembly 표준에 새 WasmGC 기능을 추가했어요. 이것은 이제 Chromium과 Firefox 브라우저에서 거의 안정화 단계에 있어요.

Dart를 Wasm 모듈로 컴파일하는 우리의 작업은 웹 앱을 위한 두 가지 하이레벨 목표를 가져요.

  • 로드 시간: Wasm으로 브라우저가 더 빨리 로드할 수 있는 배포 페이로드를 제공해서, 사용자가 웹 앱과 상호작용할 수 있는 시점까지 도달하는 시간을 개선하고 싶어요.
  • 성능: JavaScript로 구동되는 웹 앱은 좋은 성능을 위해 JIT 컴파일을 필요로 해요. Wasm 모듈은 더 저수준이고 기계어에 가까워서, 더 높은 성능을 더 적은 지터로, 더 일관된 프레임 레이트로 제공할 수 있다고 생각해요.
  • 의미론적 일관성: Dart는 지원되는 플랫폼 사이에서 매우 일관적이라는 데 자부심을 가져요. 하지만 웹에는 몇 가지 예외가 있어요. 예를 들어 Dart 웹은 현재 숫자가 표현되는 방식이 다를 수 있어요. Wasm 모듈을 쓰면 웹을 다른 네이티브 타깃과 비슷한 의미론을 가진 "네이티브" 플랫폼처럼 취급할 수 있게 돼요.

오늘 Dart-to-Wasm 컴파일의 첫 프리뷰를 발표하게 되어 기뻐요! 초기 초점은 Flutter 웹 지원이에요. 아직 이르고 완료할 작업이 많지만, 실험해 보시길 초대드리고, 우리만큼이나 흥미롭다고 느끼는지 확인해 보세요.

마무리

끝까지 읽어주셔서 감사해요. 이 글이 Dart 3에 대해 흥미를 느끼게 해줬길 바라요. Dart 3는 오늘 독립형 Dart SDKFlutter 3.10 SDK에서 모두 받을 수 있어요.

우리는 sound null safety로 Dart 언어의 대대적인 개편을 완료했고, 코어 라이브러리와 도구 정리도 마쳤어요. records와 patterns로 Dart를 더 표현력 있고 명확하게 만드는 주요 새 언어 기능도 있고요. 큰 API 표면에는 class modifiers가 세밀한 제어를 가능하게 해요. 앞으로 올 WebAssembly 지원으로 미래를 미리 볼 수도 있어요.

이 모든 기능을 통해, 우리는 Dart 3가 우리의 장기 비전을 보여준다고 생각해요. 어떤 플랫폼에서든 빠른 앱을 만들기 위한 가장 생산적인 프로그래밍 언어를 구축하는 것. 여러분도 그렇게 느껴주시길 바래요!

더 알아보기