Dart 3.13 출시 소식
Dart 3.13 출시 소식
Dart 3.13을 발표하게 되어 정말 기뻐요!
이번 릴리스는 모든 개발자가 사랑하는 주제를 기념해요. 바로 깔끔하고 가벼운 코드(clean and lightweight code)죠. Dart 3.13은 개발자 경험의 모든 층에 이 초점을 가져다줘요. 언어 쪽에서는 primary constructors가 반복되는 보일러플레이트를 없애서 클래스를 한 줄로 정의할 수 있게 해주고, 개발자 도구에서는 dart format의 개선이 Effective Dart 스타일 규칙에 맞춰 코드를 깔끔하게 유지해 줘요. dart pub에는 모노레포를 정돈하는 새 워크스페이스 명령도 추가됐고요. pub.dev에서는 <callout-box> 태그와 {@example} 지시어 같은 더 풍부한 문서 기능이 패키지 문서를 더 읽기 쉽게 만들어 줘요. 내부적으로는 네이티브 라이브러리 트리쉐이킹(@RecordUse)과 WebAssembly 지연 로딩(deferred loading)이 컴파일된 애플리케이션 바이너리를 최대한 콤팩트하게 유지하는 데 도움을 줘요.
Dart를 업데이트하거나 flutter upgrade를 실행하고, Dart 3.13에 새로 온 것들을 함께 살펴볼게요!
본문
Language updates
Primary constructors
Dart 3.12에서 실험적 프리뷰로 선보인 primary constructors가 이제 Dart 3.13에서 공식적으로 안정화됐어요!
primary constructors는 클래스 간결성에 있어 큰 진전이에요. 클래스 본문과 파라미터 목록에 필드 이름과 타입을 반복해서 쓰는 게 지루하다는 개발자들의 의견을 들었어요. primary constructors는 그 반복을 없애서, 예를 들어 두 개 필드와 생성자를 가진 Point 클래스를 한 줄로 정의할 수 있게 해줘요.
class Point(final int x, final int y);
이 기능에는 간결한 생성자 문법(concise constructor syntax)도 포함돼요. new나 factory 키워드를 쓰는 방식으로, 빈 선언 본문은 ;로 끝내는 걸 허용해요.
primary constructors를 채택하고 코드베이스 전반에서 일관된 스타일을 유지하도록 돕기 위해, 자동 수정과 함께 여러 새 린트를 도입했어요.
empty_container_bodies: 클래스류 선언에서{}대신;를 쓰도록 해 간결함을 권장해요.initialize_in_field_declaration: 필드 초기화를 primary constructor에서 필드 선언으로 옮겨 명확성을 권장해요.unnecessary_const_in_enum_constructor: enum 생성자에서const를 제거해 간결함을 권장해요.unnecessary_primary_constructor_body: 불필요한 primary constructor 본문을 제거해 간결함을 권장해요.unnecessary_type_name_in_constructor: 생성자 선언에서 타입 이름을new로 바꿔 간결함을 권장해요.use_declaring_parameters: 가능한 곳에서 declaring parameters를 쓰도록 해 간결함을 권장해요.
이런 린트와 함께, 코드 마이그레이션을 자동화하는 새 IDE 리팩터링도 추가했어요.
- Convert to primary constructor: 대상이 되는 본문 내 생성자를 primary constructor로 변환해요.
- Convert to in-body constructor: primary constructor를 본문 내 생성자로 변환해요.
- Convert to declaring parameter: primary constructor의 non-declaring 파라미터를 declaring parameter로 변환해요.
- Move initialization to the field declaration: 생성자 초기화 목록의 필드 초기화를 필드 선언으로 옮겨요.
더 알아보기: Primary constructors 문서
Web platform updates
WebAssembly 지연 로딩 프리뷰
웹에서 Dart의 WebAssembly 기능을 확장하는 일을 열심히 해왔어요. Dart 3.13에서 dart2wasm으로 컴파일할 때 지연 로딩(deferred loading) 지원의 초기 프리뷰를 공유하게 되어 기뻐요. 실험적 컴파일러 플래그로 오늘 바로 활성화할 수 있어요.
dart compile wasm -O2 --enable-deferred-loading
infoNote
지연 로딩을 쓰려면 앱 임베더가 Wasm 모듈 바이트를 로드하는 역할을 하는 콜백을 제공해야 해요. 자세한 내용은 앱이 생성한
<app>.mjs파일의CompiledApp.instantiate문서를 확인하세요.
지연 로딩을 쓰는 대형 애플리케이션에서 dart2wasm은 dart2js에 비해 상당한 초기 페이지 로드(IPL) 시간 개선을 제공해요. Flutter 웹 앱과 DOM 기반 웹 애플리케이션 모두에 이득이에요.
참고로 dart:html이나 package:js 같은 기존 웹 라이브러리는 dart2wasm에서 지원되지 않고, Dart 3.7에서 deprecated 처리됐어요 (그리고 향후 dart2js에서도 동작을 멈출 예정이에요). 많은 패키지가 이미 마이그레이션됐으므로, 패키지 의존성을 업그레이드하면 문제가 자동으로 해결되는 경우가 많아요. 하지만 코드가 이런 기존 라이브러리를 직접 쓴다면, 지원되는 dart:js_interop과 package:web 라이브러리를 채택하도록 마이그레이션 가이드를 확인해 보세요.
더 알아보기: WebAssembly 문서
Core engine and runtime updates
엔지니어링 하이라이트
웹 플랫폼 진전과 함께, 우리 팀은 Dart의 기반을 끊임없이 개선하고 있어요. 지난 석 달간 뒤에서 작업해 온 몇 가지 하이라이트를 소개할게요.
- 타입 프로모션 사운드니스 수정: 중첩 함수와 관련된 타입 프로모션의 드문 unsoundness 문제를 수정했어요.
- 공통 프런트엔드 리팩터링: 내부 공통 프런트엔드 컴포넌트를 리팩터링해서 Dart analyzer와 코드를 더 많이 공유할 수 있게 했어요.
- DDC 모듈 시스템 통합: Dart Development Compiler(DDC)에서 기존 모듈 시스템을 도구 전반에서 계속 제거해서, 단일하고 통합된 모듈 시스템을 구축하고 있어요.
- 네이티브 런타임 메모리 안전성: Dart 힙 주위에 메모리 케이지(memory cage)를 도입해 네이티브 런타임 메모리 안전성을 강화했어요.
- 동적 모듈 링킹: 동적 모듈(dynamic modules) 실험을 시작해서 동적 코드 링킹을 가능하게 하고, 팀 내 빠른 프로토타입 공유 같은 개발 워크플로 개선의 길을 열고 있어요.
Tools updates
Dart formatter
코드베이스를 깔끔하고 읽기 쉽게 유지하는 건 dart format의 최우선 과제예요. Dart 3.13에서는 포맷된 코드를 더 깔끔하게 만드는 몇 가지 스타일 조정을 도입했어요. 대부분 사소하지만, 몇 가지 핵심 업데이트는 일상적인 코드를 눈에 띄게 읽기 쉽게 만들어 줄 거예요.
- 메서드 호출 잘못된 포맷팅 수정: 최적화가 때때로 잘못 작동해서 코드를 잘못 포맷하는 버그를 수정했어요. 주로 큰 컬렉션 리터럴을 포함한 메서드 호출 주변에서 나타나는 문제였죠.
// Before:
await MethodChannelContainer()
.onMethodChannelInvoke('reportCrash', <String, Object?>{
'time': nowTime,
'errorValue': errorName,
'reason': reason,
'stacktrace': stacktrace,
});
// After:
await MethodChannelContainer().onMethodChannelInvoke(
'reportCrash',
<String, Object?>{
'time': nowTime,
'errorValue': errorName,
'reason': reason,
'stacktrace': stacktrace,
},
);
- 메서드 체인 분리 휴리스틱: 포맷터가 메서드 호출 체인을
.에서 나눌지, 인자 목록 안에서 나눌지 결정하는 휴리스틱을 바꿨어요. 메서드 체인의 대상이 단일 요소나 인자를 가진 컬렉션 리터럴이나 함수 호출이라면, 이제 대상보다 호출 체인을 나누는 쪽을 선호해요.
// Before, split the target:
function(
argument,
).method().another();
// After, split the chain:
function(argument)
.method()
.another();
- import 섹션 분리: 포맷터가 이제 import 연속체에서 서로 다른 "섹션" 사이에 빈 줄을 넣어요. 포맷터는 import를 정렬하지 않지만, Effective Dart의 규칙에 따라 이제 분리해 줘요.
// Before:
import 'dart:io';
import 'dart:math';
import 'package:args/args.dart';
import 'package:test/test.dart';
import 'my_library.dart';
// After:
import 'dart:io';
import 'dart:math';
import 'package:args/args.dart';
import 'package:test/test.dart';
import 'my_library.dart';
dart_style changelog에 다른 스타일 조정도 몇 가지 더 있지만, 위의 것들이 가장 눈에 띄는 부분이에요. 포맷팅 변경이 성가신 잡음을 유발할 수 있다는 걸 알고 있기 때문에, 스타일을 바꿀 때는 신중하려고 해요. 동시에, 스타일 개선은 코드를 더 읽기 쉽게 만들어 주는데, 생성형 AI 덕분에 어느 때보다 많은 코드를 리뷰하는 시대에 이는 값진 이점이에요. 최적화 버그 수정을 제외하고, 이런 스타일 변경은 language-versioned돼 있어서, 코드를 Dart 3.13으로 업그레이드한 뒤에만 볼 수 있어요.
더 알아보기: Dart formatter 문서
Pub updates
패키지 문서 업데이트
명확하고 구조가 잘 잡힌 패키지 문서는 건강한 생태계에 필수예요. 패키지 작성자가 더 깔끔하고 유지보수하기 쉬운 문서를 만들 수 있도록, pub.dev가 이제 <callout-box> 태그와 {@example} 지시어를 끝까지(end-to-end) 지원해요. 예제 파일에서 코드 스니펫을 직접 꺼내 API 문서에 넣으면서, 불필요한 보일러플레이트는 숨길 수 있어요.
예를 들어 example/foo.dart에서 스니펫 영역을 정의해요.
// example/foo.dart
void main() {
// #region abc
// Included in documentation
foo();
assert(false); // #hide
// #endregion
}
그런 다음 Dart doc 주석에서 그 영역을 참조해요.
/// This is a great function.
///
/// Example usage:
/// {@example /example/foo.dart#abc}
void foo() {}
pub.dev는 지정된 코드 영역을 생성된 문서에 직접 렌더링해요.
내부적으로 pub.dev는 dartdoc 파일 조회를 위한 2단계 해시 인덱스도 배포했어요. 이 업데이트는 10만 개 이상의 생성 파일을 가진 대형 패키지의 문서 렌더링을 크게 가속화해요.
더 알아보기: CupertinoActivityIndicator 클래스 문서
Native interop updates
@RecordUse와 package:record_use로 네이티브 라이브러리 트리쉐이킹
효율성에 대한 우리의 약속은 컴파일된 애플리케이션 바이너리까지 이어져요. dart:ffi와 Code Assets으로 네이티브 C, C++, Rust 코드와 인터롭하는 Dart·Flutter 앱을 빌드할 때, Dart 컴파일러는 늘 쓰이지 않는 Dart 래퍼 함수를 트리쉐이킹해 왔어요. 하지만 기반이 되는 네이티브 바이너리는 전통적으로 통째로 묶여 있었죠.
@RecordUse(package:meta에 있음)와 package:record_use를 사용하면 Dart·Flutter가 이제 네이티브 라이브러리를 Dart 코드와 함께 트리쉐이킹할 수 있어요. 이렇게 해서 최종 애플리케이션 번들에 앱이 실제로 호출하는 네이티브 코드만 담기게 돼요.
어떻게 동작하나요
- 네이티브 FFI 바인딩을
@RecordUse()로 어노테이션: 네이티브 FFI 바인딩(ffigen이 생성한 것 같은)을@RecordUse()어노테이션으로 표시해요.
import 'dart:ffi';
import 'package:meta/meta.dart';
@RecordUse()
@Native<Int32 Function(Int32, Int32)>()
external int sqlite3_open(
Pointer<Utf8> filename,
Pointer<Pointer<sqlite3>> ppDb,
);
@RecordUse()
@Native<Int32 Function(Pointer<sqlite3>)>()
external int sqlite3_close(Pointer<sqlite3> db);
-
컴파일 중 도달 가능한 호출 추적: Dart AOT 빌드의 전체 프로그램 컴파일과 트리쉐이킹 동안, 컴파일러는
@RecordUse()로 어노테이션된 바인딩 중 어떤 것이 실행 코드에서 도달 가능한지 추적해요. 트리쉐이킹으로 사라진 쓰이지 않는 Dart 코드에 있는 호출은 자동으로 제외돼요. -
링크 훅에서 쓰이지 않는 네이티브 심볼 정리: 패키지의 링크 훅(
hook/link.dart)에서input.recordedUses를 가져와요.package:record_use로 기록된 호출을 조회해서, Dart가 실제로 호출하는 심볼만 유지하도록 네이티브 툴체인(package:native_toolchain_c나 Rust 빌드 스크립트 같은)에 지시해요.
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:record_use/record_use.dart';
import 'package:my_package/src/c_library.dart';
import 'package:my_package/src/record_use_mapping.dart';
void main(List<String> arguments) async {
await link(arguments, (input, output) async {
// Extract symbols for functions called in reachable Dart code:
final symbolsToKeep = input.recordedUses?.calls.keys
.cast<Method>()
.map((method) => recordUseMapping[method.name]!);
await cLibrary.link(
input: input,
output: output,
linkerOptions: LinkerOptions.treeshake(
symbolsToKeep: symbolsToKeep,
),
);
});
}
이것이 왜 중요할까요
- 훨씬 더 작은 바이너리: SQLite, 암호화, 이미지 디코더, 오디오 엔진 같은 무거운 네이티브 의존성은 수백 개의 함수를 노출해요. 릴리스 빌드에서 링커가 참조되지 않은 네이티브 코드, 죽은 오브젝트 파일, 쓰이지 않는 export를 제거해요.
- 쓰이지 않는 네이티브 라이브러리의 완전한 생략: 애플리케이션이 패키지의 네이티브 바인딩을 전혀 호출하지 않는다면, 링크 훅이 최종 애플리케이션 번들에서 네이티브 바이너리를 완전히 제거해요.
- 매끄러운 도구 통합:
ffigen같은 도구가 생성된 바인딩을 자동으로 어노테이션하고,package:native_toolchain_c가 링크 훅과 직접 통합돼서 끝까지(end-to-end) 네이티브 트리쉐이킹이 이뤄져요.
더 알아보기: Native link hooks 문서와 Record use 문서
Wrap up
Dart 3.13은 더 단순하고 깔끔한 개발자 경험에 대한 우리의 비전을 실현해요. primary constructors 같은 표현력 있는 언어 기능을 더 똑똑한 도구, 더 풍부한 pub.dev 문서, 콤팩트한 네이티브 트리쉐이킹과 연결함으로써, 이번 릴리스는 가장 중요한 일 — 훌륭한 앱 만들기 — 에 집중할 수 있게 해줘요.
이번 릴리스의 모든 것은 여러분의 실제 현장 통찰과 버그 리포트, 패키지 기여에서 비롯됐어요. Dart 생태계는 여러분의 적극적인 참여 덕분에 번성하고 있고, 언어가 다음으로 나아갈 방향을 만드는 데 함께해 주신 것에 깊이 감사드려요.
Dart 3.13로 무엇을 만들지 너무 기대돼요. 오늘 Dart를 업데이트하거나 flutter upgrade를 실행하고, GitHub 이슈 트래커에서 대화에 참여해 주세요. 이렇게 열정적인 커뮤니티가 되어 Dart의 미래를 함께 만들어 주셔서 감사해요!
더 알아보기: Dart SDK changelog
더 알아보기
- Primary constructors 문서에서 안정화된 기능을 자세히 볼 수 있어요.
- WebAssembly 문서에서 지연 로딩과 Wasm 지원을 확인할 수 있어요.
- Dart formatter 문서에서 포맷터 변경 사항을 볼 수 있어요.
- Native link hooks 문서와 Record use 문서에서 네이티브 트리쉐이킹을 더 알아볼 수 있어요.