Dart 2.7 발표: 더 안전하고 표현력 풍부한 Dart
Dart 2.7 발표: 더 안전하고 표현력 풍부한 Dart
오늘 Dart 2.7 SDK의 안정 버전을 발표해요. 개발자를 위한 여러 새로운 기능이 담겨 있어요. 어떤 플랫폼에서든 빠른 앱을 위한 클라이언트 최적화 언어인 Dart에게 올해는 바쁜 한 해였어요. 수십 가지 새 기능과 함께 여섯 번의 새 릴리스를 내놓았죠. Dart 커뮤니티가 이런 기능들을 사용해 주는 걸 보는 건 정말 보람찬 일이고, 최근 GitHub Octoverse 보고서가 Dart를 기여자 수 기준으로 가장 빠르게 성장하는 1위 언어로 꼽아준 것도 큰 기쁨이었어요.
Dart 2.7은 확장 메서드(extension methods) 지원과, 특수 문자를 다루는 문자열을 처리하는 새 패키지를 추가해요. 또 null safety(타입 안전한 nullable·non-nullable 타입)에 대한 업데이트와, DartPad에서의 새로운 null safety 플레이그라운드 경험이 있어요. 생태계 차원에서 pub.dev에는 마음에 드는 패키지에 피드백을 남기는 새 Like 기능이 생겼어요. Dart 2.7은 오늘 dart.dev에서 SDK로 내려받을 수 있고, 오늘 출시된 Flutter 1.12에도 내장되어 있어요.
본문
확장 메서드
Dart 2.7은 오랫동안 요청받아 온 강력한 새 언어 기능을 추가해요: 바로 확장 메서드예요. 확장 메서드는 어떤 타입에도 — 여러분이 제어하지 못하는 타입에도 — 새 기능을 추가할 수 있게 해주고, 일반 메서드 호출처럼 간결하고 자동 완성도 잘 동작해요.
작은 예시를 볼게요. String에서 int와 double을 파싱하는 기능을 추가해 보죠. 앱 개발자는 String 클래스를 바꿀 수 없어요. String은 dart:core 라이브러리에 정의되어 있으니까요. 하지만 확장 메서드를 쓰면 그걸 확장할 수 있어요! 이 확장을 정의하면, String 클래스 자체에 정의된 메서드처럼 String에 새 parseInt 메서드를 호출할 수 있어요.
extension ParseNumbers on String {
int parseInt() {
return int.parse(this);
}
double parseDouble() {
return double.parse(this);
}
}
main() {
int i = '42'.parseInt();
print(i);
}
확장 메서드는 정적(static)이다
확장 메서드는 정적으로 해석되고 디스패치돼요. 즉 타입이 dynamic인 값에서는 호출할 수 없어요. 아래 코드는 런타임에 예외를 던져요.
dynamic d = '2';
d.parseInt();
→ Runtime exception: NoSuchMethodError
확장 메서드는 Dart의 타입 추론과 잘 맞아요. 아래 변수 v는 타입 String으로 추론되고, String에 대한 확장을 사용할 수 있어요.
var v = '1';
v.parseInt(); // Works!
확장 메서드는 정적으로 해석되기 때문에 정적 함수나 헬퍼 메서드를 호출하는 것만큼 빠르면서도, 훨씬 더 친숙한 호출 문법을 제공해요.
확장은 타입 변수를 가질 수 있다
짝수 인덱스의 요소를 가져오기 위한 List 확장을 정의하고 싶다고 상상해 보세요. 이 확장이 어떤 타입의 리스트에서든 동작하고, 입력 리스트와 같은 타입의 새 리스트를 반환하게 만들고 싶어요. 확장을 제네릭으로 만들고, 확장이 확장하는 타입과 확장 메서드 양쪽에 타입 파라미터를 적용하면 이렇게 할 수 있어요.
extension FancyList<T> on List<T> {
List<T> get evenElements {
return <T>[for (int i = 0; i < this.length; i += 2) this[i]];
}
}
확장 메서드는 사실 확장 멤버(extension member)다
이 기능을 확장 메서드라고 부르는 건, 다른 프로그래밍 언어에서 대응하는 언어 기능을 써 봤다면 익숙한 용어이기 때문이에요. 하지만 Dart에서는 이 기능이 더 일반적이에요. 클래스를 새 getter, setter, 연산자로 확장하는 것도 지원하거든요. 위 FancyList 예시에서 evenElements는 getter예요. 아래는 문자열을 시프트하는 연산자를 추가하는 예시예요.
extension ShiftString on String {
String operator <<(int shift) {
return this.substring(shift, this.length) + this.substring(0, shift);
}
}
커뮤니티의 훌륭한 예시들
Dart 커뮤니티의 많은 개발자가 이미 확장 메서드를 실험하고 있어요. 지금까지 본 멋진 사용 사례 몇 가지를 소개할게요.
Jeremiah Ogbomo가 만든 time 패키지는 num(int와 double의 기반 클래스)에 대한 확장을 사용해 Duration 객체를 쉽게 만들 수 있게 해줘요.
// Create a Duration via a `minutes` extension on num.
Duration tenMinutes = 10.minutes;
// Create a Duration via an `hours` extension on num.
Duration oneHourThirtyMinutes = 1.5.hours;
// Create a DateTime using a `+` operator extension on DateTime.
final DateTime afterTenMinutes = DateTime.now() + 10.minutes;
Marcelo Glasberg가 만든 i18n(국제화) 패키지는 확장 메서드로 문자열 지역화를 간단하게 해줘요.
Text('Hello'.i18n) // Displays Hello in English, Hola in Spanish, etc.
Simon Leier가 만든 dartx 패키지는 여러 핵심 Dart 타입에 대한 확장을 담고 있어요. 몇 가지 예시를 볼게요.
var allButFirstAndLast = list.slice(1, -2); // [1, 2, 3, 4]
var notBlank = ' .'.isBlank; // false
var file = File('some/path/testFile.dart');
print(file.name); // testFile.dart
print(file.nameWithoutExtension); // testFile
Brian Egan은 인기 RxDart 패키지를 확장 메서드로 업데이트해서 스트림을 다루는 API를 재정의하고 있어요.
안전한 substring 처리
Dart의 표준 String 클래스는 UTF-16 인코딩을 사용해요. 이는 프로그래밍 언어에서 흔한 선택인데, 특히 기기에서 네이티브로 실행되고 웹에서도 실행되는 것을 모두 지원하는 언어에서는 더욱 그러해요.
UTF-16 문자열은 대개 잘 동작하고, 인코딩은 개발자에게 투명해요. 하지만 문자열을 다룰 때, 특히 사용자가 입력한 문자열을 다룰 때는 사용자가 "문자 하나"라고 인식하는 것과 UTF-16에서 코드 유닛(code unit)으로 인코딩된 것 사이에 차이가 있을 수 있어요. 사용자가 입력한 문자열의 처음 세 문자를 추출하는 작은 예시를 볼게요.
var input = ['Resume'];
input.forEach((s) => print(s.substring(0, 3)));
$ dart main.dart
Res
지금까지는 문제 없어요. 입력 목록의 문자열에서 처음 세 문자를 출력했고 결과는 Res예요. 이제 다양한 지역의 사용자가, 액센트나 한글, 그리고 'resume'이라는 개념을 나타내는 이모지 조합까지 담을 수 있는 문자열을 입력한다고 생각해 볼게요.
// New longer input list:
var input = ['Resume', 'Résumé', '이력서', '💼📃', 'Currículo'];
$ dart main.dart
Res
Ré
이력서
💼�
Cur
음, 몇 개는 잘 동작했는데 Résumé와 💼📃에는 무슨 일이 일어난 걸까요? Résumé는 왜 "두 글자"짜리 문자열이 나온 걸까요? 💼📃의 이상한 물음표는 또 뭘까요? 여기서 문제는 Unicode의 어두운 구석에 있어요. Résumé의 액센트 있는 é는 실제로 두 개의 코드 포인트예요. e와 결합하는 급성 액센트(combining acute accent)인 거죠. 그리고 📃, 페이지와 컬이 있는 이모지는 단일 코드 포인트인데 우연히 U+d83d U+dcc3의 서로게이트 쌍(surrogate pair)으로 인코딩된 거예요. 헷갈리시죠?
말했듯이, 보통은 문자와 코드 포인트에 대해 걱정할 필요가 없어요. 문자열 전체를 받고, 주고받고, 전달하기만 한다면 내부 인코딩은 투명해요. 하지만 문자열의 문자를 순회하거나 문자열 내용을 조작해야 한다면 곤란해질 수 있어요. 좋은 소식은 Dart 2.7이 이런 경우를 다루는 새 패키지 characters를 도입한다는 거예요. 이 패키지는 사용자가 인식하는 문자 시퀀스, 즉 Unicode grapheme cluster로 본 문자열을 지원해요. characters 패키지로 텍스트를 줄이는 코드를 아주 조금만 바꿔서 고칠 수 있어요.
// Before:
input.forEach((s) => print(s.substring(0, 3)));
// After, using the characters package:
input.forEach((s) => print(s.characters.take(3)));
먼저 편리한 .characters 확장 메서드를 써서 s의 문자열에서 새 Characters 인스턴스를 만들어요. 그런 다음 그럴듯한 take() 메서드로 처음 3개 문자를 추출해요.
이 새 패키지의 기술 프리뷰가 pub.dev에 있어요. 이 패키지에 대한 여러분의 생각을 듣고 싶어요. 문제를 발견하면 보고해 주세요.
Null safety 프리뷰
몇 달 전 우리는 Dart에서 null safety를 지원하겠다는 의도를 발표했어요. null 참조 예외를 일으키지 않으면서 객체 참조에 안전하게 접근하는 지원을 추가하겠다는 거였죠. 오늘 우리는 null safety 정적 분석을 미리 볼 수 있는 방법을 알려드려요. 동기를 설명할 작은 예시를 볼게요.
void main() {
Person('Larry', birthday: DateTime(1973, 03, 26)).describe();
Person('Sergey').describe();
}
class Person {
String firstName;
DateTime birthday;
Person(this.firstName, {this.birthday});
void describe() {
print(firstName);
int birthyear = birthday?.year;
print('Born ${DateTime.now().year - birthyear} years ago');
}
}
이 코드를 실행하면 두 번째 사람을 설명할 때 null 포인터 예외로 크래시가 나요. 그 사람은 birthday가 설정되어 있지 않기 때문이죠. 우리가 코딩 실수를 한 거예요. 생성자에서 birthday 필드를 선택적으로 만들고 birthday?.year로 null birthday를 검사해서 생일을 모르는 사람도 어느 정도 예상하긴 했지만, birthyear가 null인 경우를 처리하는 걸 잊어버린 거예요.
이 코드를 새 null safety 플레이그라운드에 붙여 넣어 볼게요. 이 플레이그라운드는 null safety 기능의 정적 분석 부분에 대한 기술 프리뷰를 담고 있는 특별한 빌드의 DartPad예요. 코드를 실행하지도 않고도 세 가지 문제를 볼 수 있어요.
이 분석 오류들을 고치면서 null safety의 이점을 누리기 시작할 수 있어요. null safety 플레이그라운드에서 다음 편집을 시도해 보세요(결국엔 이 안전한 코드에 도달하게 돼요).
birthday가 null일 수 있음을 선언하려면DateTime birthday를DateTime? birthday로 바꾸세요.birthday가 null일 때birthyear가 null일 수 있음을 선언하려면int birthyear를int? birthyear로 바꾸세요.- 마지막 print 호출을 null 검사로 감싸세요:
if (birthyear != null) {…}.
이 예시가 null safety에서 우리가 원하는 경험을 잘 보여주길 바라요. 언급했듯이 이 플레이그라운드는 null safety의 일부에 대한 초기 기술 프리뷰일 뿐이에요. 아직 만드는 중이니까요. 우리는 Dart SDK에서 null safety의 첫 베타 릴리스를 완성하기 위해 열심히 작업하고 있어요. 베타를 위해 작업 중인 내용은 다음과 같아요.
- nullable·non-nullable 참조의 완전한 구현 완성
- null safety를 Dart의 타입 추론과 스마트 프로모션에 통합(예: 할당이나 null 검사 이후 nullable 변수에 안전하게 접근 허용)
- 어떤 타입이 nullable이고 어떤 타입이 non-nullable인지 선언하도록 Dart 핵심 라이브러리 이식
- Dart 앱과 패키지를 이식하는 업그레이드 작업의 대부분을 자동화할 수 있는 마이그레이션 도구 추가
이 작업이 완료되면 베타 SDK에서 사용할 수 있게 하고, 여러분의 앱과 패키지에서 이 기능의 이점을 누리기 시작할 수 있을 거예요. null safety 플레이그라운드도 기능이 구현됨에 따라 새 기능으로 계속 업데이트할 계획이에요.
많은 개발자가 null safety를 사용할 수 있게 되자마자 쓰고 싶어할 거라고 확신하지만, 편할 때 언제든 마이그레이션해서 준비가 되면 그 기능을 선택할 수 있어요. 아직 기능을 선택하지 않은 라이브러리·패키지는 기능을 선택한 라이브러리에 의존할 수 있고 그 반대도 마찬가지예요.
null safety에 대해서는 앞으로 몇 달 안에 더 많은 이야기를 나눌 거예요. 전환을 준비하는 더 자세한 조언도 포함해서요.
pub.dev에서 패키지 좋아요(👍) 누르기
오늘 pub.dev에서도 패키지용 새 Like 기능이 출시돼요. 이건 마음에 드는 패키지를 나타내는 새로운 "사람 신호(human signal)"를 도입하는 거예요. 패키지를 좋아하려면 패키지 상세 정보 옆의 엄지척 아이콘을 클릭하기만 하면 돼요.
현재는 좋아요 수를 전반적인 점수 모델에 반영하진 않지만, 이후 릴리스에서 그렇게 할 계획이에요. 일반 검색 UI와 목록 페이지도 시각적으로 개편해서 패키지의 "좋아할 만한 정도"를 부각시킬 계획이에요.
감사의 말
Dart 팀을 대표해 여러분과 Dart 커뮤니티의 모든 분께 지속적인 지원에 감사드려요! 계속해서 피드백을 주고, Dart 토론과 커뮤니티에 참여해 주세요. Dart 커뮤니티가 주는 지원이 없다면 우리는 제대로 기능하는 오픈소스 프로젝트가 될 수 없어요.
Dart에게 2019년은 믿을 수 없을 만큼 흥미로운 한 해였지만, 우리는 여기서 멈추지 않아요. 2020년에는 대담한 계획이 있어요. dart:ffi와 null safety 같은 기능의 안정 버전을 출시하고 새 기능을 도입하는 것까지요. 오늘부터 Dart 2.7을 사용해 보시길 권해요. dart.dev와 오늘의 Flutter 1.12 릴리스, 그리고 최근 재설계된 DartPad에서 사용할 수 있어요.