5000년만의 포스팅..ㅎㅎ
올해 초 플러터 3.7이 공개되었다. 그래픽 퍼포먼스 개선, Material 3, pattern matching 등 여러 업데이트 사항이 있었지만 그 중 이상하게도 젤 흥미로웠던 element embedding에 대해 파헤쳐 봄!
간단하게 말해서 네이티브 웹에 플러터 앱을 집어넣을 수 있는 기능이다. 물론 플러터는 크로스플랫폼 앱인 만큼 기존에도 웹앱으로 동작하긴 했다. 하지만 이전에는 플러터 앱이 html의 body 전체를 오버라이딩하는 방식으로만 작동했었는데, 이번 업데이트로 일반 웹 내부에 앱을 컴포넌트처럼 넣어서 보여줄 수 있다는 뜻!
플러터에서 제공한 샘플 웹
개발자도구로 확인해보니 전체 프레임과 왼쪽 버튼들은 html로 구성된 웹 영역이고, 오른쪽의 카운터 앱 화면은 플러터 앱이다. 더 신기한 건 CSS로 플러터 앱 영역을 조절할 수도 있고, 또 네이티브 웹이나 플러터 앱에서 일어나는 인터랙션들이 서로 영향을 끼침..!(카운트 숫자가 바뀌는 등)
앱에서 웹뷰를 띄워 자바스크립트 핸들러로 통신하는 건 해봤어도 웹에 플러터 앱을 넣어 이렇게 간단하게 상호작용이 되는 게 너무 신기했다.
예제만으로 호기심이 충족되지 않아서(+재밌어보여서) 직접 해봤당. 새로운 기능이라 그런지 구글링해봐도 아직 자료가 별로 없어서 처음엔 좀 헤맸다. 웹 프로젝트를 만들어서 다트 파일을 추가하는 건지, 아니면 플러터 앱에 html을 추가하는 건지... 대체 어디에 뭘 해서 합치는 거지? 싶었는데, 플러터 프로젝트에 기본적으로 생성되는 웹앱용 index.html 파일을 수정해서 만들 수 있었다.
방법을 찾고 나니까 생각보다 간단했음! html이랑 자바스크립트를 홀랑 다 까먹어버려서 고생했을 뿐...
flutter create [프로젝트명]
web 폴더에 있는 index.html을 열고, html을 수정해 원하는 웹 형태를 구성한다. 나는 style.css 파일도 따로 만들어서 연결해줌!
플러터 앱을 임베딩하고 싶은 영역에 <div>
태그를 추가하고, id를 붙여준다. 나는 app_area라고 명명했다.
<script type="text/javascript" src="script.js"></script>
<scrpit>
내부의 코드는 복사한 뒤 index.html에서 삭제하고, 4번에서 생성한 스크립트 파일에 붙여넣은 후 다음과 같이 수정한다. 기존 코드는 그냥 바디 전체에서 플러터 앱이 돌도록 했는데, querySelector로 3번에서 선언한 div를 선택하고 그 안에서 플러터 앱을 넣도록 고친 것!// before
window.addEventListener('load', function (ev) {
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function (engineInitializer) {
engineInitializer.initializeEngine().then
(function (appRunner) {
appRunner.runApp();
});
}
});
});
// after
window.addEventListener('load', function (ev) {
var appArea = document.querySelector("app_area");
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function (engineInitializer) {
engineInitializer.initializeEngine({
hostElement: appArea,
}).then(function (appRunner) {
appRunner.runApp();
});
}
});
});
그리고 flutter run을 해서 크롬으로 열면...!
아무일도 일어나지 않았다...!🥲
뭐가 잘못된 건지 몰라서 한참동안 이것저것 바꾸면서 원인을 찾았는데 어이없을 만큼 간단한 문제였다. 앱이 들어갈 사이즈를 지정안해서..ㅎㅎ;;
#app_area {
width: 320px;
height: 480px;
}
css 파일에서 픽셀을 지정해주고 새로고침하니까 앱이 나타났다! 🎉🎉🎉
다음으로 자바스크립트 영역과 플러터 앱 영역이 통신할 수 있도록 해보자.
우선 프로젝트에 js 패키지를 추가한다.
flutter pub add js
이때 중요한 것! 패키지 버전이 0.6.7 이상이어야 하는데, 만약 dart sdk와 충돌이 나서 안 된다면... sdk 버전을 3.0.0 이상으로 업그레이드시켜줘야 한다. 현재 해당 sdk는 베타채널로만 풀려있는데(23.02.20 기준) 나는 채널을 옮겨서 업그레이드해줌!
다트에서 정의한 변수, 함수 등을 자바스크립트단에서 사용할 수 있도록 export해보자. state 전체를 내보내서 모든 필드에 접근 가능하도록 할 수 도 있고, 특정한 메서드만 지정해서 내보낼 수도 있다.
// main.dart
()
class _MyAppState extends State<MyApp> {
...
void initState() {
final stateToexport = createDartExport(this);
setProperty(globalThis, 'myAppState', stateToexport);
super.initState();
}
...
}
State에 @JSExport
어노테이션을 붙여주고, createDartExport(this)
로 export할 객체를 생성한다. 그리고 setProperty
메서드에 (1) globalThis*, (2) JS에서 사용할 변수명, (3) 생성한 객체를 인자로 넣어 호출한다.
*globalThis는 각 환경에서 접근할 수 있는 전역 객체를 가리킨다고 함!
그리고 자바스크립트 내에서 사용할 때는 window 객체를 통해 위 (2)에서 설정한 이름으로 접근하면 된다.
// script.js
window.myAppState.(함수 혹은 변수명)
// main.dart
void initState() {
final setThemeColorToExport = allowInterop(setThemeColor);
setProperty(globalThis, 'setThemeColor', setThemeColorToExport);
super.initState();
}
void setThemeColor(List rgbList) {
setState(() {
themeColor = Color.fromRGBO(rgbList[0], rgbList[1], rgbList[2], 1);
});
}
export할 메서드를 allowInterop
이라는 함수에 넣어 이를 할당하고, 마찬가지로 setProperty
에 (1) globalThis, (2) JS에서 사용할 함수명, (3) 생성한 객체를 넣어 호출한다.
나는 테마 컬러를 바꾸는 메서드를 생성해 JS에서 컨트롤할 수 있도록 넘겼다.
사용할 때는 똑같이 window 객체를 이용해 호출할 수 있음!
// script.js
function updateColor(e) {
let bgColor = getComputedStyle(e.currentTarget).backgroundColor;
let rgbList = bgColor.split("(")[1].split(")")[0].split(", ").map((e) => parseInt(e));
window.setThemeColor(rgbList);
}
updateColor라는 함수 내부에서 다트 함수를 호출하도록 했고, 이벤트리스너로 색상별 버튼에 달아줬다.
플러터앱에서 자바스크립트 함수를 사용하는 건 더 쉽다! 우선 스크립트 파일에서 window
객체에 원하는 함수를 정의한다. (함수명 앞에 'window.' 붙이기)
나는 버튼을 눌러 카운터 숫자가 올라갈 때마다 웹의 input에서도 해당 값이 반영되도록 하는 함수를 정의했다.
// script.js
let counter = document.querySelector("#counter");
window.setCounter = (count) => {
counter.value = count;
}
그리고 다트 파일로 돌아와 callMethod
함수에 (1) globalThis, (2) JS에서 정의한 함수명, (3) 전달할 파라미터 리스트를 넣어 호출하면 됨! 이때 당연히 정의한 함수의 파라미터(인자)와 전달하는 파라미터의 형식을 맞춰줘야 한다.
// main.dart
void _incrementCounter() {
setState(() {
_counter++;
});
callMethod(globalThis, 'setCounter', [_counter]);
}
그렇게 쪼금 더 다듬어서 완성된 플러터 웹~~~ 호스팅도 완료 >>> 바로가기 🔍
굳이 호스팅까지 한 이유는! 다시 벨로그를 성실하게 쓰고.. 결과물 앱 페이지들을 저기 추가하겠다는 나만의 포부...💪🏻🌟
📚 References
Whats Next For Flutter
Element Embedding in Flutter Web
dart:js library - Dart API
dart:js_util library - Dart API
와.. 플러터 신기해요