Flutter로 개발하다보면 결제나 지도와 같이 Javascript로 작성된 웹 페이지와 데이터를 주고 받아야 하는 경우가 생깁니다. 이번 포스트에서는 Flutter 모바일과 웹 모두에서 웹 페이지 간 통신 방법에 대해 알아보겠습니다.
dependencies:
flutter:
sdk: flutter
...
webview_flutter: ^4.4.2
먼저, 모바일에서 WebView를 사용하기 위한 pubspec.yaml
에 webview_flutter
패키지 종속성을 추가합니다.
flutter:
...
assets:
- assets/view.html
Flutter에서 html 파일을 사용하기 위해 pubspec.yaml
에 해당 문구를 추가합니다.
<!DOCTYPE html>
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100%;
width: 100%;
padding: 20px;
padding-top: 36px;
display: flex;
justify-content: center;
align-items: center;
}
.input-container {
position: relative;
max-width: 500px;
width: 100%;
}
.text-input {
width: 100%;
height: 36px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
.button {
width: 100%;
height: 36px;
margin-top: 16px;
background-color: #007BFF;
color: #fff;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="input-contianer">
<input type="text" class="text-input" id="text-input" placeholder=" ">
<button class="button">전송</button>
</div>
</body>
</html>
assets/lib
경로에 텍스트 필드와 버튼으로 구성된 view.html
을 생성합니다.
먼저 lib/widget
에 2가지 파일을 만들어야합니다.
하나는 모바일에서 사용할 input_webview_mobile.dart
, 하나는 웹에서 사용할 input_webview_web.dart
입니다.
파일을 2가지로 나누어 모바일과 웹 위젯을 따로 만드는 이유는, webview_flutter
패키지가 웹 환경을 지원하지 않아 웹에서는 HtmlElementView
으로 구현해야하기 때문입니다.
input_webview_mobile.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class InputWebView extends StatefulWidget {
const InputWebView({super.key});
State<InputWebView> createState() => _InputWebViewState();
}
class _InputWebViewState extends State<InputWebView> {
late final WebViewController controller;
void initState() {
super.initState();
controller = WebViewController()
..loadFlutterAsset('assets/view.html');
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('이름 입력하기')),
body: WebViewWidget(
controller: controller,
),
);
}
}
input_webview_web.dart
import 'dart:ui' as ui;
import 'dart:html' as html;
import 'package:flutter/material.dart';
class InputWebView extends StatefulWidget {
const InputWebView({super.key});
State<InputWebView> createState() => _InputWebViewState();
}
class _InputWebViewState extends State<InputWebView> {
void initState() {
super.initState();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'input-webview',
(int viewId) => html.IFrameElement()
..style.width = '100%'
..style.height = '100%'
..src = 'assets/view.html'
..style.border = 'none',
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('이름 입력하기')),
body: HtmlElementView(
viewType: 'input-webview',
),
);
}
}
두 위젯 모두 클래스명을
InputWebView
으로 만들고, 조건부 import를 이용해 플랫폼에 따라 위젯을 분기해줘야 합니다. 이 방법은 후술하겠습니다.
완성한 모바일, 웹 각각의 화면은 다음과 같습니다.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_webview_communication/widget/input_webview_mobile.dart'
if (dart.library.html) 'package:flutter_webview_communication/widget/input_webview_web.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () => _showWebView(context),
child: Text('이름 입력하기'),
),
),
);
}
Future<void> _showWebView(BuildContext context) {
return Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => InputWebView()));
}
}
import 'package:flutter_webview_communication/widget/input_webview_mobile.dart'
if (dart.library.html) 'package:flutter_webview_communication/widget/input_webview_web.dart';
다음 부분에서 input_webview_mobile.dart
import하고, 만약 dart.library.html
을 사용하는 경우 input_webview_web.dart
을 import하게 됩니다.
kIsWeb
만으로 분기하기에는 여러가지 문제점이 있기 때문에 다음과 같이 조건부 import를 이용해서 분기해줍니다.
자세한 내용은 다음 링크를 확인해주세요.
먼저 각 InputWebView
에 Javascript Callback을 추가하고, view.html
에서 버튼을 눌렀을 때 이벤트를 등록하는 방식으로 구현합니다.
input_webview_mobile.dart
void initState() {
super.initState();
controller = WebViewController()
... (중략)
..addJavaScriptChannel('messageHandler', onMessageReceived: (message) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text(message.message)),
);
})
..loadFlutterAsset('assets/view.html');
}
..addJavaScriptChannel('messageHandler', onMessageReceived: (message) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text(message.message)),
);
})
다음 문구를 통해 WebViewController
에 JavascriptChannel
을 추가합니다. messageHandler
라는 JavascriptChannel
을 만들고, messageHandler
채널을 통해 메시지를 수신하면 Callback이 호출됩니다.
Callback이 호출되면 해당 메시지를 AlertDialog
로 표시합니다.
view.html
<script>
const button = document.querySelector("button");
const input = document.querySelector("input");
button.addEventListener("click", (event) => {
messageHandler.postMessage(input.value);
});
</script>
</body>
뒤에 다음의 스크립트를 작성합니다. 버튼이 클릭되면, postMessage
를 사용해 messageHandler
JavascriptChannel
에 메세지를 전송합니다.
input_webview_web.dart
void initState() {
super.initState();
... (중략)
html.window.onMessage.listen((event) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text(event.data)),
);
});
}
view.html
에서 postMessage
가 호출될 때 실행되는 이벤트 Callback을 작성하고 Callback이 호출되면 해당 메시지를 AlertDialog
로 표시합니다.
view.html
<script>
const button = document.querySelector("button");
const input = document.querySelector("input");
button.addEventListener("click", (event) => {
window.parent.postMessage(input.value, '*');
});
</script>
브라우저의 부모 창에 postMessage
를 사용해 메시지를 전송합니다.
view.html 페이지 로드가 끝났을 때, 각 InputWebView
에서 postMessage
를 보내는 방식으로 구현합니다.
input_webview_mobile.html
void initState() {
super.initState();
controller = WebViewController()
... (중략)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (value) => {
controller.runJavaScript(
'messageHandler.postMessage(\'hello\', \'*\')')
}),
);
}
NavigationDelegate
라는 콜백 함수 델리게이트를 이용해 웹 페이지의 상태가 로드가 완료된 상태이면, controller.runJavaScript('messageHandler.postMessage(\'hello\', \'*\')')
으로 웹 페이지에 메시지를 전달합니다.
view.html
messageHandler.addEventListener('message', (event) => {
alert(event.data);
});
messageHandler
라는 JavascriptChannel
으로 전달된 메시지가 있을 때, 이벤트가 발생되어 해당 메시지를 경고 대화 상자로 표시합니다.
input_webview_web.dart
void initState() {
super.initState();
... (중략)
html.window.postMessage('hello', '*');
}
브라우저에 postMessage
를 이용해 메시지를 전달합니다.
view.html
window.parent.addEventListener('message', (event) => {
console.log(event);
alert(event.data);
});
부모 창에서 전달된 메시지가 있을 때, 이벤트가 발생되어 해당 메시지를 경고 대화 상자로 표시합니다.
이제, Flutter와 웹 페이지 사이 양방향 통신이 가능한 앱이 완성되었습니다.