Dart의 JS interop 역사
Dart의 JS interop 역사
Wasm 지원이 방금 현재 Flutter 베타에 들어왔어요. 이는 Dart 3.3에서 달성한 흥미로운 JavaScript interop 이정표 덕분이에요. 이를 기념하며, 10년에 걸친 Dart와 JS 상호운용성의 여정을 돌아볼게요.
AI 이미지: Gemini로 생성
상호운용성(interoperability)은 Dart의 시작부터 핵심 초점이었어요. Dart가 2011년 처음 출시됐을 때 *임베디드 가능(embeddable)*하고 멀티 플랫폼이 되도록 설계됐어요. 독립형 가상 머신에서 실행되고, 브라우저에 임베디드되며, JavaScript로 컴파일됐어요. 2015년 Flutter가 등장했을 때, 우리는 그곳에도 임베디드할 준비가 되어 있었어요. 이제 우리는 WasmGC 런타임을 타깃으로 하는 것도 기대하고 있어요.
처음에 우리는 Dart가 임베디드된 각 플랫폼의 기능을 노출하기 위해 빠르게 작업했어요. 그렇게 우리 SDK의 플랫폼별 라이브러리가 탄생했어요. VM에서 파일 시스템을 노출하는 dart:io, 웹에서 브라우저 API를 노출하는 dart:html 등이 그 예시예요. 이 라이브러리들은 일반 Dart 라이브러리처럼 보이고 느껴졌지만, 내부에는 이를 동작시키기 위한 정교한 저수준 네이티브 프리미티브가 숨어 있었어요. 이것이 우리가 처음 발명한 최초의 interop 형태였어요. 표현력이 있었지만 SDK 라이브러리로만 제한됐어요.
웹에서 개발자들은 브라우저 API 이상이 필요했어요. 그래서 우리는 더 많은 타깃을 커버하도록 상호운용성을 여는 방법을 찾기 시작했어요. 출발점으로 2013년에 JavaScript 라이브러리에 접근할 수 있게 해주는 dart:js를 도입했어요.
// Short example JavaScript code to illustrate Dart/JS interop
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
// Access via `dart:js` (2013)
import 'dart:js' as js;
void main() {
// This line has a typo! oops :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// This call fails with a noSuchMethod because method2
// returns an int, oops
object.callMethod('method2', []).substr(1);
}
그 당시 우리는 dart:js가 우리가 원하는 프로그래밍 모델이 아니라는 걸 알고 있었어요. JavaScript의 이름에 접근하려면 문자열을 사용해야 했어요. 컴파일 타임에 문제를 찾는 것은커녕, 코드 완성도 생각할 수 없었죠! 구현도 비쌌어요. 대부분의 연산에서 박스와 깊은 복사(deep copy)에 크게 의존했거든요. 그래서 우리는 2014년과 2015년에 package:js 관련 아이디어를 계속 구상하다가 마침내 v0.6이 출시됐어요.
// Access via `package:js` (2015)
import 'package:js/js.dart';
// Magic annotations allow us to declare API signatures:
@JS()
class MyObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
// Access to code is less error prone: analyzer can check that
// these symbols match a declaration, and we get code-completion too!
var object = myTopLevel;
object.field1 = 1;
// But types are not checked, this unsoundly invokes substring on an int
object.method2().substring(1);
}
package:js를 통해 마침내 효율적이고 사용자 친화적인 공개 API를 갖게 됐어요. 추상 클래스에 몇 가지 주석을 뿌리기만 하면 JavaScript API에 접근할 수 있었어요. 모든 것이 마법처럼 작동했어요. 작동이 멈추기 전까지는요. package:js로 할 수 없는 것이 많았어요. 브라우저 API 직접 접근, 멤버 이름 바꾸기, 변환, Dart 로직 연결 등이요. 자세한 내용은 여기에 있어요. 이를 보완하기 위해 우리는 dart:js와 비슷한 가볍고 효율적인 저수준 API인 dart:js_util도 출시했어요.
package:js의 모든 한계는 정말 신경 쓰였지만, 우리 손이 묶여 있었어요. 더 잘하려면 Dart 언어에서 더 많은 것이 필요했어요. 그 무렵 우리는 언어에서 가장 큰 변화를 위해 이미 작업하고 있었어요 — Dart를 sound하게 만드는 작업이었죠. 아이러니하게도 2018년 Dart 2.0으로 새 타입 시스템을 출시했을 때 상호운용성은 더 나빠졌어요! 초기 한계를 넘어서, package:js를 특별하게 만든 그 마법에는 어두운 면이 있었어요 — 타입의 유효성을 확인할 수 없었죠. 이는 우리의 상호운용성이 달리 sound한 언어에서 unsoundness의 원인이 된다는 뜻이었어요.
그때부터 우리의 여정은 Dart와 JS-interop을 공동 노력으로 함께 개선하는 데 초점을 맞추게 됐어요. 명확한 원칙(관용적이고, 표현력 있고, 구성 가능하며, 정확하고, 접근 가능하고, 실용적이며, 비마법적이고, 완전한)에 따라 우리는 타이핑과 정적 디스패치에 기반을 둔 설계로 나아갔고, 이는 Dart 언어에 도전장을 던졌어요. 이어서 나란히 진화하는 과정이 이어졌어요.
2019년 Dart 2.7은 정적 확장 메서드를 추가했어요. 래퍼 없이도 JS Promise를 Dart Future로 변환하는 등 JS-interop 클래스에 사용자 지정 Dart 로직을 붙이고 값을 변환할 수 있었어요. 2021년에 우리는 package:js v0.6.4와 함께 @staticInterop을 출시했어요. 마침내 JS-interop은 더 이상 dart:html 같은 SDK 라이브러리가 전용으로 관리하던 브라우저 API를 노출할 만큼 표현력이 생겼어요. 2023년 Dart 3.0에서 unsound null safety를 없앴을 때, 우리는 드디어 이룬 진전을 볼 수 있었고, 우리의 설계와 @staticInterop 작업은 오랜 soundness 격차를 해결할 준비가 됐다는 것을 분명히 보여줬어요.
그 해에 우리는 WasmGC 컴파일을 도입하고 JS-interop을 활용해 Flutter web 같은 풍부한 프레임워크를 그 위에서 실행했어요. 이는 프로그래밍 모델에서 Dart와 JS 경계를 명확히 정의하고, Wasm과 JS 컴파일 타깃 모두에서 JS로 작업하는 일관된 방법을 찾기 위한 JS Types 작업에 불을 붙였어요. 우리는 또한 extension types 언어 실험을 시작했어요 — Dart 3.3에 출시된 기능으로, Dart 언어와 JS-interop 사이의 격차를 메워요. 수년 동안 JS-interop은 타입 소거(type erasure) 같은, Dart의 다른 무엇과도 맞지 않는 동작을 갖고 있었어요. extension types 덕분에 JS-interop은 마침내 관용적이 되고 Dart 개발 도구에서 마땅히 받아야 할 지원을 받을 수 있게 됐어요.
길을 따라 많은 변화와 굴곡이 있었지만, 10년 내내 일관되게 유지된 것이 하나 있었어요 — 바로 Dart 커뮤니티의 적극적인 참여예요. 커뮤니티 구성원들은 초기 단계에서 dart:js를 테스트하고 기여했으며, 나중에는 package:js의 설계에 영향력을 행사했어요. 기능 격차를 해결하는 도구를 작성했고(package:js_wrapping), Dart API를 자동 생성해 생산성을 개선하는 방법을 실험했어요(package:js_facade_gen, package:js_bindings, package:typings).
각 기여는 Dart의 interop 설계를 더 좋게 만드는 데 도움이 됐어요. 여러분 모두에게 이토록 흥미로운 모험을 만들어 준 것에 감사드려요! 마침내 2024년이 됐어요. 우리는 dart:js_interop을 package:web과 함께 Dart 3.3에서 출시했어요. 이는 Dart에서 JS interop을 위한 최신 솔루션이며, Flutter를 Wasm으로 컴파일하는 것을 가능하게 해요.
// Access via `dart:js_interop` (2024)
import 'dart:js_interop';
// Declarations use extension types, which are very similar to package:js
// declarations. The main difference: they are statically dispatched.
extension type MyObject._(JSObject _) implements JSObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
var object = myTopLevel;
object.field1 = 1;
// At last, access is sound - this line fails with a type error
// when returning from method2.
object.method2().substring(1);
}
dart:js_interop은 extension types에 기반한 정적이고, sound하며, 관용적이고, 표현력 있고, 일관된 interop 형태예요. 어떤 JavaScript나 브라우저 API도 노출할 수 있어요. package:web는 dart:js_interop을 사용해 13년 전 dart:html이 했던 일을 하지만, JavaScript와 WasmGC 모두에서 지원되는 방식으로 해요. 오늘 우리는 새로운 형태의 Dart/JS interop과 그것이 가능하게 하는 미래를 기념하게 되어 기뻐요. 우리의 과거를 알기에, 이것이 여정의 끝이 아니라 우리 역사의 흥미로운 지점이라는 것을 확신해요. 여러분이 무엇을 만들지 정말 기대돼요!
더 알아보기
dart:js_interop— Dart 3.3의 최신 JS interop 솔루션package:web—dart:html을 대체하는 웹 라이브러리- JS Types — Dart와 JS 경계 정의
- Extension types — JS-interop을 관용적으로 만든 언어 기능