Introduction & Overview
웹 페이지에서 구현되어 있는 SNS 로그인(카카오와 네이버)가 Flutter의 웹뷰에서 제대로 되지 않는 현상이 발생하였다.
마침내 웹뷰 자체에서 자바스크립트의 window.open(), window.close() 를 직접 제어해줘야 한다는 것을 알아냈다.
내가 사용한 웹뷰는 InAppWebView이므로, 이 위젯을 기준으로 설명하려 한다.
이 글은 InAppWebView 위젯으로 이미 웹 페이지를 표시하고는 있으나,
웹 페이지 내 팝업 처리, SNS 로그인(카카오, 네이버 등)에 대한 해결 방법을 제시하는 포스팅이다.
또한, iOS 환경에서는 확인하지 않았으며 오로지 안드로이드에서만 확인하였다.
InAppWebView의 사용 방법은 아래의 링크를 참고하길 바란다.
포스팅에서 설명하는 프로젝트는 아래의 깃허브 주소에서 다운로드 가능합니다.
팝업을 띄울 임시 html 파일 정의
버튼을 누르면 내 블로그가 팝업으로 출력되는 예시 샘플 html 코드다.
이 html 코드를 가지고 팝업을 띄우도록 할 예정이다.
아래의 코드는 웹뷰 초기 페이지 데이터에 넣을 계획이므로 따로 html 파일을 생성하지 않아도 된다.
어떤 내용인지 대략적으로 파악해보자.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popup Test</title>
<script>
function doOpen() { window.open('https://luvris2.tistory.com'); }
</script>
</head>
<body>
<center>
<input type=button value="팝업 띄우기" onclick=doOpen() style="width:200px;height:200px;font-size: xx-large;">
</body>
</html>
버튼을 누르면 새로운 팝업 창이 생기며, 새로 생긴 창에 내 블로그가 출력되는 절차이다.
기본 InAppWebView 설정
팝업을 띄우기 위해서는 설정해 줘야 하는 부분이 세 군데 있다.
- InAppWebViewGroupOptions
- 아래의 샘플 코드의 옵션을 그대로 사용하면 되므로 따로 다시 건들일은 없다.
- onCreateWindow
- 기본 뼈대만 구성하고 아래의 챕터에서 좀 더 자세히 확인해보도록 한다.
- onCloseWindow
- 팝업이 출력되는 레이아웃에서 해당 기능을 구현한다.
샘플 코드로 뼈대와 팝업을 띄우기 위한 기본 설정을 정의해보자.
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
void main() {
runApp(const MaterialApp(
home: Scaffold(body: SafeArea(child: InAppWebViewPage()))));
}
class InAppWebViewPage extends StatelessWidget {
const InAppWebViewPage({super.key});
@override
Widget build(BuildContext context) {
return InAppWebView(
// 팝업 허용을 위한 옵션 설정
initialOptions: InAppWebViewGroupOptions(
// 모든 플랫폼 공용 옵션
crossPlatform: InAppWebViewOptions(
javaScriptEnabled: true, // 자바스크립트 사용 여부
javaScriptCanOpenWindowsAutomatically: true, // 팝업 여부
),
// 안드로이드 플랫폼 옵션
android: AndroidInAppWebViewOptions(
supportMultipleWindows: true, // 멀티 윈도우 허용
),
),
initialData: InAppWebViewInitialData(data: """
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popup Test</title>
<script>
function doOpen() { window.open('https://luvris2.tistory.com'); }
</script>
</head>
<body>
<center>
<input type=button value="팝업 띄우기" onclick=doOpen() style="width:200px;height:200px;font-size: xx-large;">
</body>
</html>
"""),
onCreateWindow: (controller, createWindowAction) async {
// 팝업을 띄울 때의 제어 코드 작성
return true;
},
);
}
}
- 실행화면
팝업 출력에 대한 로직은 따로 구현하지 않았기 때문에 버튼을 눌러도 팝업이 출력되지 않는다.
팝업 띄우기 제어하기 (onCreateWindow)
showDialog에 새로 띄울 팝업의 페이지를 생성해 넣어주면 된다.
다만, onCreateWindow의 파라미터인 createWindowAction을 이용하여야 한다.
WindowPopup이라는 페이지가 있다고 가정하고
createWindowAction 파라미터의 값을 해당 페이지에 전달해주도록 코드를 짜보자.
return InAppWebView(
// 코드 생략
onCreateWindow: (controller, createWindowAction) async {
// 팝업을 띄울 때의 제어 코드 작성
showDialog(
context: context,
// WindowPopup 페이지에 createWindowAction 값을 넘겨주기
builder: (context) => WindowPopup(windowAction: createWindowAction),
);
return true;
},
);
팝업을 띄우기 위한 레이아웃 정의
이번엔 위에서 팝업이라고 정의한 'WindowPopup' 페이지를 정의해보자.
값 초기화
이 페이지는 createWindowAction 의 값을 전달받을 수 있어야 하기 때문에 생성자에 해당 값을 초기화해주어야 한다.
팝업의 웹뷰에서는 createWindowAction의 windowId를 이용해야한다.
기능 설계
팝업의 형태는 결국 웹 페이지가 스택에 또 하나 추가되는 개념이므로,
새로운 레이아웃에 또 다시 웹 뷰를 정의해주면 된다.
단, 이번에 웹뷰에서는 onCloseWindow도 구현해주어야 출력된 팝업을 닫아 다시 원래의 페이지로 돌아갈 수 있다.
이 부분이 특히 내가 가장 애를 먹었던 SNS 콜백 함수를 처리하여 카카오나 네이버 로그인을 할 수 있도록 해준다.
레이아웃 설계
이 레이아웃은 정해진 것이 아니고 개발자가 입맛대로 짜면 된다.
이 포스팅에서는 간단하게 풀스크린이며,
왼쪽 상단에 X버튼으로 팝업을 종료할 수 있는 레이아웃으로 만들어보겠다.
코드
// windowPopup.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class WindowPopup extends StatefulWidget {
// 메인 웹뷰에서 보낸 createWindowAction 값 저장 변수
final CreateWindowAction createWindowAction;
// 생성자 초기화
const WindowPopup({Key? key, required this.createWindowAction}) : super(key: key);
@override
State<WindowPopup> createState() => _WindowPopupState();
}
class _WindowPopupState extends State<WindowPopup> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Stack(
children: [
// 팝업 표시를 위한 웹뷰 설정
InAppWebView(
// 메인 웹뷰에서 받은 windowId
windowId: widget.createWindowAction.windowId,
// 팝업 닫기 기능 구현
onCloseWindow: (controller) {
Navigator.pop(context);
},
),
// X 모양 아이콘 터치 시 팝업 닫기
Align(
alignment: Alignment.topLeft,
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 40)),
),
],
),
),
);
}
}
전체 소스 코드
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_inappwebview_popup_test/windowPopup.dart';
void main() {
runApp(const MaterialApp(
home: Scaffold(body: SafeArea(child: InAppWebViewPage()))));
}
class InAppWebViewPage extends StatelessWidget {
const InAppWebViewPage({super.key});
@override
Widget build(BuildContext context) {
return InAppWebView(
// 팝업 허용을 위한 옵션 설정
initialOptions: InAppWebViewGroupOptions(
// 모든 플랫폼 공용 옵션
crossPlatform: InAppWebViewOptions(
javaScriptEnabled: true, // 자바스크립트 사용 여부
javaScriptCanOpenWindowsAutomatically: true, // 팝업 여부
),
// 안드로이드 플랫폼 옵션
android: AndroidInAppWebViewOptions(
supportMultipleWindows: true, // 멀티 윈도우 허용
),
),
initialData: InAppWebViewInitialData(data: """
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popup Test</title>
<script>
function doOpen() { window.open('https://luvris2.tistory.com'); }
</script>
</head>
<body>
<center>
<input type=button value="팝업 띄우기" onclick=doOpen() style="width:200px;height:200px;font-size: xx-large;">
</body>
</html>
"""),
onCreateWindow: (controller, createWindowAction) async {
// 팝업을 띄울 때의 제어 코드 작성
showDialog(
context: context,
builder: (context) =>
WindowPopup(createWindowAction: createWindowAction),
);
return true;
},
);
}
}
windowPopup.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class WindowPopup extends StatefulWidget {
// 메인 웹뷰에서 보낸 createWindowAction 값 저장 변수
final CreateWindowAction createWindowAction;
// 생성자 초기화
const WindowPopup({Key? key, required this.createWindowAction})
: super(key: key);
@override
State<WindowPopup> createState() => _WindowPopupState();
}
class _WindowPopupState extends State<WindowPopup> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Stack(
children: [
// 팝업 표시를 위한 웹뷰 설정
InAppWebView(
// 메인 웹뷰에서 받은 windowId
windowId: widget.createWindowAction.windowId,
// 팝업 닫기 기능 구현
onCloseWindow: (controller) {
Navigator.pop(context);
},
),
// X 모양 아이콘 터치 시 팝업 닫기
Align(
alignment: Alignment.topLeft,
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 40)),
),
],
),
),
);
}
}
확인
버튼을 눌러 팝업을 띄우고, 팝업에서 보이는 'X' 버튼으로 pop하여 이전 화면으로 돌아가는 절차이다.
이 확인 화면에서는 onCloseWindow의 메서드가 사용되지 않았지만,
뒤로 가기 버튼 및 자바스크립트 window.open()과 window.close()를 사용하는 소셜 로그인은 해당 기능을 반드시 구현해야 한다. (그래야 흰 화면만 나오지 않고 팝업이 window.close()로 닫혀서 로그인이 정상적으로 진행된다.)