WebView와 하이브리드 — Flutter 안에서 웹 콘텐츠 표시
WebView와 하이브리드 — Flutter 안에서 웹 콘텐츠 표시
결제 페이지, 약관, 외부 콘텐츠 등 웹 페이지를 앱 안에서 표시해야 하는 경우가 많습니다. Flutter의 WebView로 웹 콘텐츠를 임베딩하고 JavaScript와 통신하는 방법을 정리해보겠습니다.
설치
dependencies:
webview_flutter: ^4.8.0
# 플랫폼별 구현체
webview_flutter_android: ^3.16.0
webview_flutter_wkwebview: ^3.14.0 # iOS
기본 사용
import 'package:webview_flutter/webview_flutter.dart';
class WebViewScreen extends StatefulWidget {
final String url;
const WebViewScreen({super.key, required this.url});
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
class _WebViewScreenState extends State<WebViewScreen> {
late final WebViewController _controller;
bool _isLoading = true;
int _progress = 0;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
setState(() => _isLoading = true);
},
onPageFinished: (url) {
setState(() => _isLoading = false);
},
onProgress: (progress) {
setState(() => _progress = progress);
},
onNavigationRequest: (request) {
// 특정 URL은 외부 브라우저로
if (request.url.startsWith('https://external.com')) {
launchUrl(Uri.parse(request.url));
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onWebResourceError: (error) {
print('에러: ${error.description}');
},
),
)
..loadRequest(Uri.parse(widget.url));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('웹뷰'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
LinearProgressIndicator(
value: _progress / 100,
),
],
),
);
}
}
JavaScript 통신
Flutter → JavaScript 호출
// JavaScript 코드 실행
await _controller.runJavaScript('''
document.body.style.backgroundColor = 'lightblue';
alert('Flutter에서 호출!');
''');
// JavaScript 실행 후 결과 받기
final result = await _controller.runJavaScriptReturningResult('''
document.title;
''');
print('페이지 제목: $result');
JavaScript → Flutter 호출
// JavaScript에서 Flutter로 메시지 보내기
_controller.addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
print('JS에서 받은 메시지: ${message.message}');
// 메시지에 따라 처리
final data = jsonDecode(message.message);
if (data['action'] == 'close') {
Navigator.pop(context);
} else if (data['action'] == 'payment_complete') {
_handlePaymentComplete(data['orderId']);
}
},
);
// 웹 페이지의 JavaScript에서 호출
// FlutterChannel.postMessage(JSON.stringify({action: 'close'}));
// FlutterChannel.postMessage(JSON.stringify({action: 'payment_complete', orderId: '12345'}));
HTML 문자열 로드
_controller.loadHtmlString('''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: sans-serif; padding: 16px; }
.highlight { color: blue; font-weight: bold; }
</style>
</head>
<body>
<h1>Flutter WebView</h1>
<p class="highlight">HTML 문자열을 직접 로드할 수 있습니다.</p>
<button onclick="FlutterChannel.postMessage('버튼 클릭됨')">
Flutter에 메시지 보내기
</button>
</body>
</html>
''');
네비게이션 제어
// 뒤로 가기
if (await _controller.canGoBack()) {
await _controller.goBack();
} else {
Navigator.pop(context);
}
// 앞으로 가기
if (await _controller.canGoForward()) {
await _controller.goForward();
}
// 현재 URL 확인
final currentUrl = await _controller.currentUrl();
뒤로가기 버튼 처리
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (await _controller.canGoBack()) {
await _controller.goBack();
} else {
if (context.mounted) Navigator.pop(context);
}
},
child: WebViewWidget(controller: _controller),
)
쿠키 관리
final cookieManager = WebViewCookieManager();
// 쿠키 설정
await cookieManager.setCookie(
const WebViewCookie(
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
),
);
// 모든 쿠키 삭제
await cookieManager.clearCookies();
실전: 결제 페이지
class PaymentWebView extends StatefulWidget {
final String paymentUrl;
final Function(String orderId) onPaymentComplete;
const PaymentWebView({
super.key,
required this.paymentUrl,
required this.onPaymentComplete,
});
@override
State<PaymentWebView> createState() => _PaymentWebViewState();
}
class _PaymentWebViewState extends State<PaymentWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'PaymentBridge',
onMessageReceived: (message) {
final data = jsonDecode(message.message);
if (data['status'] == 'success') {
widget.onPaymentComplete(data['orderId']);
Navigator.pop(context);
}
},
)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (request) {
// 결제 앱 스킴 처리
final url = request.url;
if (url.startsWith('intent://') ||
url.startsWith('kakaotalk://') ||
url.startsWith('supertoss://')) {
launchUrl(Uri.parse(url));
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.paymentUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('결제')),
body: WebViewWidget(controller: _controller),
);
}
}
면접 포인트: 한국 결제(PG)에서는 카카오페이, 토스 등 외부 앱 호출이 필요합니다. intent://, 커스텀 스킴을 NavigationDelegate에서 가로채서 launchUrl로 외부 앱을 실행해야 합니다.
정리
webview_flutter로 웹 콘텐츠를 앱 안에 임베딩합니다addJavaScriptChannel로 JavaScript ↔ Flutter 양방향 통신이 가능합니다NavigationDelegate로 URL 이동을 제어하고 외부 앱 호출을 처리합니다- 결제 페이지에서는 외부 앱 스킴 처리가 필수입니다
loadHtmlString으로 HTML 문자열을 직접 렌더링할 수도 있습니다
댓글 로딩 중...