티스토리 뷰

Webview 양방향 통신 비동기 -> 동기 전환 (w. Flutter)

webview_flutter 라이브러리를 사용하면서 Flutter를 이용하여 Device의 위치 기능, 정보 등 다양한 기능을 사용하기 위해 webview_flutter의 내장 함수 ..addJavaScriptChannel()로 post Message를 보내고 .runJavascript()를 통해 웹의 JavaScript를 동작하여 양방향 통신 하였습니다.

그러나 다음과 같은 문제가 발생하게 되었습니다.

  1. .runJavascript() 동작이 안드로이드에서는 되고 iOS에서는 되지 않는 문제

  1. 비동기적으로 실행되며 .runJavascript()가 Flutter에서 실행되어 웹이 수시로 상태가 변화하지 못하는 문제

그래서 1번과 같은 문제로 인해 1번 문제 해결 방안에 대해서 찾아보고 질문도 남겼지만 정확한 답변을 얻지 못해서 아쉬워서 새로운 방안을 찾은 거였지만 또 며칠 후에 다시 시도해 보니 동작하는 것을 보고.runJavascript()에 대한 믿음이 떨어졌고 상태변화와 시점을 정확하게 파악하기 위해 비동기를 동기로 바꾸고 싶어 방법을 변경하게 되었습니다.

문제 해결에 앞서 현재 만들려고 하는 애플리케이션이 Flutter의 웹뷰로 웹/앱을 내장하는 방식으로 개발을 진행하기 때문에 웹뷰, 웹/앱 부터 현재 애플리케이션이 어떻게 구성되어 있는지 이해하면 더 좋을 것 같아서 작성하게 되었습니다. 😄

 

웹뷰와 웹/앱

웹뷰(WebView)

안드로이드 및 iOS 개발에 주로 사용되며 모바일 애플리케이션 내부에 웹 컨텐츠를 표시하기 위한 컴포넌트입니다. 웹뷰를 통해 모바일 앱 내에서 웹 기술을 사용하여 동적인 컨텐츠를 표시하고나 웹/앱을 내장할 수 있습니다.

웹/앱(Web App)

사용자가 인터넷을 통해 액세스할 수 있는 모든 종류의 앱을 포함하며 웹 브라우저를 통해 실행되는 애플리케이션입니다. 웹/앱은 일반적으로 웹 기술 (HTML, CSS, JavaScript 등)을 사용하여 개발되며, 모바일 브라우저나 데스크톱 브라우저를 통해 실행됩니다.

 

애플리케이션 설명

웹뷰로 웹/앱을 내장한 애플리케이션을 만들기 위해 개발한 웹을 서버에 띄우고 Flutter의 webview_flutter라이브러리를 통해 구현하였습니다.

추가로 웹뷰로 웹/앱을 내장하는 것 같이 서버에 띄어져 있는 웹을 계속 보는 것이 아닌 서버에 띄어져 있는 웹에 대한 정보들을 가져와서 Flutter에 내장하는 방식입니다.

 

비동기 상황 (현재)

맨 처음 이야기한 것처럼 webview_flutter 라이브러리를 사용하면서 Flutter를 이용하여 Device의 위치 기능, 정보 등 다양한 기능을 사용하기 위해 webview_flutter의 내장 함수 ..addJavaScriptChannel()로 post Message를 보내고 .runJavascript()를 통해 웹의 JavaScript를 동작하여 양방향 통신비동기로 진행하였습니다.

그림을 보면 webview_flutter의 라이브러리를 통해서 양방향 통신을 한 것을 볼 수 있습니다.

[이미지] 이미이미3

코드 (main.dart)

사용 라이브러리

  • geolocator: ^10.0.0 //Device의 경도, 위도를 파악
  • webview_flutter: ^4.2.2 //웹뷰 실행
import 'dart:convert';
import 'dart:io';
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:geolocator/geolocator.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); //앱 실행할 준비가 완료될 때까지 기다린다.
  runApp(const MyHomePage());
}

class MyHomePage extends StatelessWidget {
  static WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..addJavaScriptChannel(
      'Message',
      onMessageReceived: (JavaScriptMessage msg) {
        var data = jsonDecode(msg.message);
        switch (data["cmd"]) {
          case "get_location":
            await Permission.locationWhenInUse.request(); // 위치 권한 요청 및 설명
            getLocation();
        }
      }
    )
    ..loadRequest("서버에 띄어져 있는 웹 URL");

  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '비동기 접근 부분',
      home: Scaffold(
        body: WebViewWidget(
          controller: controller
        )
      )
    );
  }

  static Future<String> getLocation() async {
    LocationPermission permit = await Geolocator.checkPermission();
    String permit_str = permit.toString();

    if(permit_str != "LocationPermission.denied" && permit_str != "LocationPermission.deniedForever") {
      Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
      String lat = position.latitude.toString();
      String lon = position.longitude.toString();
      controller.runJavaScript('window.get_location($lat, $lon)');
    } else {
      controller.runJavaScript('window.get_location("0", "0")');
    }
  }
}

웹 JS

function get_location() {
  //flutter 통신
  Message.postMessage(
    JSON.stringify({
      cmd: 'get_location',
    })
  );

  //flutter에서 .runJavaScript()로 실행되는 부분
  window.get_location = (lat, lon) => {
    console.log(lat, lon);
  };
}

그래서 되었다가 안 됐다가 하는 부분, 동기적으로 Device의 기능을 사용하기 위해 다음과 같은 방법을 사용하려고 합니다.

 

동기 상황 (해결책)!!

webview_flutter을 사용하지만, 웹과 연결하는 것만 사용하고 Flutter 내부에서 Server를 띄워서 URL을 통해 동기적으로 통신하는 방안입니다.

그림을 보면 webview_flutter에서 web을 내장하고 Server와 통신하는 것을 볼 수 있습니다.

코드

사용 라이브러리

  • geolocator: ^10.0.0 //Device의 경도, 위도를 파악
  • webview_flutter: ^4.2.2 //웹뷰 실행

main.dart에서 HttpServer을 통해 Server를 띄워 URL을 통해(API에 접근하는 방식과 같이) 웹에서 fetch등을 이용하여 양방향 통신을 하며 동기적으로 동작하게 하는 방식입니다.

main.dart

import 'dart:convert';
import 'dart:io';
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:geolocator/geolocator.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); //앱 실행할 준비가 완료될 때까지 기다린다.
  runApp(const MyHomePage());

  try {
    var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080).then((_server) {
      _server.listen((HttpRequest request) async {
        HttpResponse response = request.response;
        response.headers.add("Access-Control-Allow-Origin", "*"); //Cors 처리
        response.headers.add("Access-Control-Allow-Methods", "POST,GET,DELETE,PUT,OPTIONS"); //Cors 처리

        switch (request.uri.path) {
          // uri.path로 접근하게 하기
          // 웹의 JS 코드에서 fetch("HttpServer에 쓴 주소 + /get_location")을 통해 접근 가능
          case "/get_location":
            await Permission.locationWhenInUse.request(); // 위치 권한 요청 및 설명
            String location_info = await getLocation();
            response.write(location_info);
            break;
        };
        response.close();
      });
    }); // 서버 띄우기
  } catch(e) {
    print("mattabu: Something wrong server"+e.toString()+"");
  }
}

Future<String> getLocation() async {
  LocationPermission permit = await Geolocator.checkPermission();
  String permit_str = permit.toString();

  if(permit_str != "LocationPermission.denied" && permit_str != "LocationPermission.deniedForever") {
    Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
    String lat = position.latitude.toString();
    String lon = position.longitude.toString();
    return '{"LATITUDE": "'+lat+'", "LONGITUDE": "'+lon+'"}';
  } else {
    return '{"LATITUDE": "0", "LONGITUDE": "0"}';
  }
}

class MyHomePage extends StatelessWidget {
  static WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..loadRequest("서버에 띄어져 있는 웹 URL"); //webview에 웹 내장시키기

  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '동기 접근 부분',
      home: Scaffold(
        body: WebViewWidget(
          controller: controller
        )
      )
    );
  }
}

웹 JS

다음과 같이 접근하여 값을 얻을 수 있습니다.

function flutter_get_location() {
      try {
        const response = await fetch("http://localhost:8080/get_location", {
            method: "GET",
        });
        const result = await response.json();
        return result;
    } catch (xhr) {
        // fetch로 접근 조차 실패한 부분만 error
        console.error("request 에러:", xhr);
    }
}

 

마치며

웹뷰 개발을 진행하면서 안드로이드, iOS 모든 상황에서 똑같이 동작하게 해야 하며 CSS도 많이 수정하면서 여러 개를 알면서 좋았고 웹이라면 express와 같은 Server를 띄워서 URL로 전역에서 관리할 방법을 flutter 내부에서도 사용하니 신선했습니다.
그러나 서버를 띄운다는 것은 유지 보수, 복잡성, 대기시간, 보안등 많은 문제를 야기하니 좀 더 생각하며 기술을 선택해야겠다는 생각이 많이 들었고 새로운 방법이 있으면 배우고 싶습니다. 😄
기본적인 방법 이외의 것을 개발하게 되어 나누고 싶었고 좀 더 오래 기억하고 싶어 글을 작성하게 되었는데 약간 회사의 제품 관련 코드여서 온전하게 다 보여드릴 수 없는 점이 아쉬운 것 같습니다.
도움이 필요하신 분들은 댓글이나 이메일로 연락해 주세요. 😸

긴 글 읽어주셔서 감사합니다.

 

REF

https://medium.com/@naik.rpsn/http-server-running-on-a-mobile-app-with-flutter-1ef1e717dda1
https://medium.com/@naik.rpsn/http-server-running-on-a-mobile-app-with-flutter-1ef1e717dda1
https://api.flutter.dev/flutter/dart-io/HttpServer-class.html
https://nhj12311.tistory.com/595
https://api.flutter.dev/flutter/dart-io/HttpRequest-class.html
https://stackoverflow.com/questions/10456591/cors-with-dart-how-do-i-get-it-to-work
https://dart-ko.dev/language

반응형
공지사항
최근에 올라온 글