Dart 튜토리얼 27편: JavaScript interop 현대 방식(dart:js_interop·JS types·package:web)
요약
핵심 요지
- 문제 정의: 웹에서는 브라우저 API와 JS 라이브러리를 함께 써야 하는데, Dart 값과 JS 값은 서로 다른 도메인이라 타입/변환/런타임 제약을 모르고 섞으면 오류가 난다.
- 핵심 주장: next-gen JS interop은
external1 선언과 interop 타입(예: extension type)으로 JS 멤버를 명시적으로 선언하고, JS types(JSString등)와 변환(toJS/toDart)으로 값 이동을 통제하는 방식이 기준이다. - 주요 근거:
@JS()2로 글로벌 멤버를 선언하는 예시, extension type로 JS 타입을 표현하는 예시,toJS/toDartInt변환 예시,package:web로dart:html을 대체/마이그레이션하는 예시가 제시된다. - 실무 기준: “(1) JS API를 global scope에 노출 → (2) Dart에서 external 멤버 선언 → (3) 변환이 필요한 값만 변환 → (4) 브라우저 API는
package:web로 접근” 순서로 진행한다.
문서가 설명하는 범위
- JS interop의 개요와 학습 진입점
- interop 멤버(
external)와 interop 타입(확장 타입 기반) 선언 방식 - JS types 계층과 변환(toJS/toDart) 및 제약(런타임 타입 체크, external 타입 제한)
package:web로 브라우저 API에 접근하고dart:html에서 마이그레이션하는 이유/절차
읽는 시간: 16분 | 난이도: 초급
참고 자료
문제 상황
웹 앱은 브라우저 API와 JS 생태계를 함께 사용합니다.
하지만 Dart 값과 JS 값은 서로 다른 언어 도메인이고, Wasm 타깃에서는 런타임도 분리될 수 있다고 설명됩니다.
그래서 “아무 값이나 넘기기”가 허용되지 않고, 타입과 변환 규칙을 따라야 합니다.
해결 방법
단계 1: interop는 “글로벌 JS scope 노출 → external 선언”으로 시작한다
Why
NOTEJS 라이브러리를 쓰려면 먼저 JS 쪽에서 사용할 API가 접근 가능한 위치에 있어야 합니다.
그 다음 Dart에서 그 멤버를 선언하고 호출할 수 있습니다.
What
NOTE이 방식에서는 “JS API를 global JS scope 어딘가에 노출한 뒤”, Dart에서
externalinterop 멤버로 접근합니다.
또한 top-level interop 멤버는@JS()로 표시해야 한다고 설명합니다.
How
TIP예시로 JS의
globalThis.name과 함수isNameEmpty를 만들고, Dart에서 getter/setter/함수로 선언하는 코드가 제시됩니다.@JS()external String get name;@JS()external set name(String value);@JS()external bool isNameEmpty();
Watch out
WARNINGtop-level
external은dart:ffi같은 다른 의미의external과 구분해야 하므로@JS()가 필수라는 설명이 제시됩니다.
즉, annotation을 빼면 interop 의도가 전달되지 않습니다.
결론: JS interop는 “JS 글로벌 노출 → Dart @JS() + external 선언” 흐름으로 시작합니다.
단계 2: interop 타입은 extension type로 “JS 값에 붙일 Dart 인터페이스”를 만든다
Why
NOTEJS 값은 구조가 있지만, Dart에서 타입이 없으면 자동 완성/정적 검사/설계 의도가 흐려지기 쉽습니다.
따라서 JS 값에 대응하는 Dart 타입을 만들어두는 것이 유리합니다.
What
NOTEinterop 타입은 Dart가 제공하는 JS 타입(
JSObject등)을 감싸는 extension type로 선언할 수 있다고 설명됩니다.
또한 “런타임에서 실제 Window라는 보장”이 없다는 점도 함께 설명됩니다.
How
TIP다음은
JSObject를 표현 타입으로 갖는 interop 타입 예시입니다.extension type Window(JSObject _) implements JSObject {}
Watch out
WARNING런타임 보장이 없으므로, 특정 JS 타입임을 가정하고 사용하면 위험할 수 있습니다.
따라서 필요한 경우 interop를 통해 타입을 확인하는 방법을 별도로 사용해야 한다는 안내가 있습니다.
결론: interop 타입은 “타입 안정성/가독성”을 위해 만들되, 런타임 보장 여부를 구분해야 합니다.
단계 3: JS types와 toJS/toDart 변환으로 “값 이동”을 통제한다
Why
NOTE값을 Dart↔JS로 넘기는 순간, 성능과 의미(참조/복사) 문제가 생길 수 있습니다.
그래서 변환은 “필요할 때만” 명시적으로 수행하는 기준이 필요합니다.
What
NOTEJS types는
JS접두사를 가진 타입(JSString,JSNumber등)이며, Dart 값과 JS 값을 컴파일 시점에 구분하기 위한 체계로 설명됩니다.
변환은 Dart→JS는toJS, JS→Dart는toDart...형태로 제공된다고 설명됩니다.
How
TIP변환 예시는 다음과 같이 제시됩니다.
String str = 'hello world';JSString jsStr = str.toJS;JSNumber jsNum = ...;int integer = jsNum.toDartInt;
Watch out
WARNINGJS↔Dart 변환은 컴파일 타깃(JS vs Wasm)에 따라 비용/의미가 달라질 수 있고,
List같은 값은 참조/프록시/복사 여부를 가정하지 말라고 경고합니다.
즉, 변환 후 두 값 사이의 연결 관계에 기대지 말아야 합니다.
결론: 값 이동은 toJS/toDart로 통제하고, 변환의 비용/의미 차이를 전제로 코드를 작성합니다.
단계 4: 브라우저 API는 package:web로 접근하고, dart:html에서 마이그레이션한다
Why
NOTEWasm 타깃을 고려하면, 기존
dart:html같은 코어 웹 라이브러리는 제약이 될 수 있습니다.
그래서 장기적으로는package:web로 이동하는 방향이 제시됩니다.
What
NOTE
package:web는 브라우저 API 접근을 제공하고, Wasm 호환을 위해dart:js_interop기반으로 설계되었다고 설명됩니다.
또한dart:html등은 deprecated이며 Wasm에서 지원되지 않는다고 안내합니다.
How
TIP기본 사용 예시는 다음과 같이 제시됩니다.
코드 블록 주석은 한글로 맞췄습니다.import 'package:web/web.dart';void main() {final div = document.querySelector('div')!;div.text = 'Text set at ${DateTime.now()}';}마이그레이션은
dart:htmlimport를 제거하고package:web/web.dart로 바꾸는 흐름이 제시됩니다.import 'dart:html' as html; // 제거import 'package:web/web.dart' as web; // 추가
Watch out
WARNING
package:web는List를 그대로 반환하지 않는 API가 있고, 런타임 타입 테스트(is)가dart:html과 다르게 동작할 수 있다는 안내가 있습니다.
즉, 마이그레이션은 import만 바꾸는 작업이 아니라, 타입/컬렉션/테스트 방식까지 함께 바꾸는 작업이 될 수 있습니다.
결론: 브라우저 API 접근은 package:web로 표준화하고, 마이그레이션 시 타입/리스트/테스트 차이를 함께 처리합니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!