웹뷰(WebView) 한번 써보자. (with. Flutter)

개똥이개똥이
7 min read

웹뷰란 무엇인가…

웹뷰가 무엇인지는 굉장히 많은 글들에서 다루고 있으므로 짧게 정의를 내려본다면,

네이티브(혹은 크로스플랫폼) 앱 위에서 동작하는 하나의 웹브라우저 라고 생각할 수 있다.

그렇기 때문에 swift, kotlin, flutter, RN 등 앱을 위한 언어 혹은 프레임워크에서 웹뷰를 제공하는데, 장점으로는 웹으로 개발된 페이지를 빠르게 앱으로 빌드할 수 있다는 장점이 있고, 단점으로는 단말기 OS 버전에 따른 호환성 이슈나 각 OS 별로 다른 UI의 형태를 띈다던가 하는 이슈가 생긴다.

만들어둔 웹을 통해 빠르게 앱을 빌드할 수 있다는 장점 외에는 단점이 수두룩한 느낌이 강하고 실제로 한국 외 해외의 APP시장은 네이티브(크로스 플랫폼도 잘 안쓰는 것 같다) 위주로 개발이 되는 경향이 큰 것 같으나 유감스럽게도 한국의 한정된 개발풀에서는 웹을 만드는 것도 지칠 수 밖에 없는 지 웹뷰를 적극적으로 활용하고 있는 곳이 많았다.

직접적으로 아직 사용해보진 않아서 장단점을 자세하게 알 수는 없지만, 세간에 떠도는(?) 장단점을 가볍게 정리하자면

장점

  • 만들어둔 웹을 활용하는 것이기 때문에 빠른 개발이 가능하다.

  • 웹페이지가 업데이트 되면 웹뷰 상에서 당연히 바로 반영되므로 앱스토어를 통한 업데이트가 필요없다.

단점

  • 네이티브 APP의 경우 로딩되는 당시에 이미 사용할 준비를 마치기 때문에 사용성에 있어 훨씬 쾌적한 경험을 주지만 웹뷰는 웹페이지를 불러오는 것이기 때문에 느리다.

  • 인터넷이 연결되지 않은 상황에는 웹뷰는 보이지 않게 된다.

  • 각종 OS, 버전에 따른 호환성 이슈가 있다.

  • 웹뷰로만 구성된 APP은 스토어 심사가 어렵다.

심지어 위의 장점 중 두번째로 작성한 바로 반영되는 것의 경우, RN, 플러터 기반 크로스플랫폼 앱에서는 코드푸시를 지원하기 때문에 웹뷰만의 장점으로 보기 어렵다.

웹뷰가 네이티브에 비해서 편리하다는 장점은 있지만, 결국은 웹뷰보다는 네이티브로 개발하는 게 더 낫다는 생각이 팍팍든다.

But…

그렇지만 개발자가 나뿐인 스타트업이라면?

앱만들어, 네 제가요..?

맞다. 회사에 개발자라곤 나 혼자다. 처음에는 나도 플러터로 만들고 싶다는 생각이 그득그득했다. 하지만 마감일정, 보안, 플러터학습 등등 모든 것들을 고려해보니 플러터로 앱 전체를 만드는 것은 거의 불가능하다는 생각이 들었다. 그 때 생각난 것이 한정된 자원에서 가장 효율적으로 작업할 수 있는 웹뷰였다.

나는 처음에는 웹뷰가 단순히 웹을 보여주는 역할에 그치는 것이라고 생각했다. 이름 그대로 “WebView”로만 생각한 것이다. 심지어 처음 시도해본 웹뷰에서 자바스크립트 허용옵션을 false로 두는 바람에 깨져버린 UI를 보고 아 이게 말로만 듣던 웹뷰의 극악함인가? 하고 지레 겁먹으며 그냥 네이티브로 개발해야겠다.. 하는 생각을 하고 뒷걸음질 쳤다.

근데 이게 웬걸..? 걍 내가 못한 거였음..ㅎ

심지어 APP과 WEB 사이 양방향 통신도 가능하다고 하니 앱일 때는 앱을 위한 UI만 보여주는 것이 충분할 것 같다는 생각이 들었다. 아마 대부분이 이런 방식으로 웹뷰를 활용하고 있겠지만.. 나는 그 어디에도 제대로 조언을 구할 수가 없는 개발자 1인의 스타트업…

한번 부딪혀보자는 생각으로 웹뷰 공부를 시작했다.

웹뷰 사용해보기

웹뷰를 사용하기 위해서는 기본적으로 App이 필요하다.

나는 Flutter를 활용해서 앱을 개발해보고자 하고 있기 때문에, Flutter를 기반으로 웹뷰를 사용해보겠지만, 웹뷰를 개발하는 것에 대한 편의는 Flutter보다는 RN에 이점이 더 있다는 글을 보았다. 그러므로 여러가지 본인의 환경에 맞추어 개발을 하면 될 것 같다.

WebView를 위한 세팅

우선 iOS에서의 세팅은 생략하도록 하겠다. 이유는… 내가 당장 안써서 기록을 안하는 것. 그리고 자료가 온갖데 널렸다. 찾아보면 된다.

아무튼 안드로이드에서 웹뷰를 사용하기 위해서는 인터넷에 접근할 수 있는 권한을 주어야한다.

// 플러터 폴더 내 /android/app/src/main/AndroidManifest.xml

<manifest xmlns:android = "..." package = "...">
// 인터넷 사용 권한 획득
// user-permission을 추가하는데, 이 태그 내용으로 권한들을 추가할 수 있다.
    <user-permission android:name = "android.permission.INTERNET" /> 
    <application ...>
        ...
        <activity ...>
            ...
        <activity />    
    <application />
<manifest />

또한 Sdk의 최소 버전과 컴파일 버전을 명시해줘야 한다.

// 플러터 폴더 내 /android/app/build.gradle

android {
    compileSdkVersion 32 // 컴파일 버전
    ...
    defaultConfig {
        ...
        // 최소 버전, 일반적으로 20으로 세팅을 하며
        // 웹뷰 라이브러리 문서를 살펴보면 최소 사용조건이 있다. 고려해서 작성하면 된다.
        minSdkVersion 20
        ...
    }
}

Webview 라이브러리

Flutter 공식 지원 ) webview-flutter

플러터팀에서 공식으로 지원하고 있는 웹뷰 라이브러리를 webview-flutter 이다. 현재까지 4.4.2 버전까지 나와있으며, WebView Widget을 제공하던 3.x.x 버전과는 다르게 Controller 기반으로 동작한다. inappwebview에서 가장 많이 사용되는 웹뷰와 메소드들을 정리해서 만들어둔 패키지라고 해도될 만큼 필수 기능이 들어있다는 것이 장점이지만 새창 열기가 안된다는 단점이 있다.

3.0.4

// webview_flutter 3.0.4
// 가장 기본적인 동작

// WebView 위젯을 통해 웹뷰 컴포넌트를 생성한다.
// initialUrl에 string 형식의 url을 작성한다.

class WebviewPage extends StatefulWidget {
  const WebviewPage({super.key});

  @override
  State<WebviewPage> createState() => _WebviewPageState();
}

class _WebviewPageState extends State<WebviewPage> {
  @override
  Widget build(BuildContext context) {
        // WebView 위젯을 통해 웹뷰 컴포넌트를 생성한다.
        // initialUrl 은 웹뷰로 보여주고 싶은 페이지의 URL을 넣어주면 되고
        // javascriptMode를 통해 자바스크립트 사용을 허용할 지 설정해줄 수 있다.
    return const WebView(
      initialUrl: "https://soye-portfolio.vercel.app/",
      javascriptMode: JavascriptMode.unrestricted,
    );
  }
}

4.4.2

// webview_flutter 4.4.2
// 가장 기본적인 동작

// main.dart

void main() {
    // 4버전 부터는 `WidgetsFlutterBinding.ensureInitialized()`을 통해 
    // 플러터 프레임워크가 앱을 실행할 준비가 될 때까지 기다리게 한다.
    // main함수 내부에 별도의 코드 없이 runApp만 있다면 
    // `WidgetsFlutterBinding.ensureInitialized()`코드가 내부적으로 실행이 되지만
    // webview_flutter 4버전부터는 안정성을 위해 명시적으로 넣어 사용하도록 한다.
  WidgetsFlutterBinding.ensureInitialized();

  runApp(MaterialApp(
    home: Home(),
  ));
}
// webview_page.dart

class WebviewPage extends StatefulWidget {
  const WebviewPage({super.key});

  @override
  State<WebviewPage> createState() => _WebviewPageState();
}

class _WebviewPageState extends State<WebviewPage> {
    // 기존 3버전에서는 WebView 위젯에 인자를 넣어 옵션들을 활성화했지만
    // 4버전부터는 컨트롤러를 통해 조작하고 WebViewWidget 위젯에 컨트롤러를 전달해주면 된다. 

    // 또한 기존에는 string을 url값으로 넘겨주었으나 4버전부터는 Uri()를 활용하거나 하여
    // uri값을 넘겨줘야한다. (Uri()의 이점은 쿼리문을 편하게 사용할 수 있게해주는 것.)
  WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..loadRequest(uri);

  @override
  Widget build(BuildContext context) {
    return WebViewWidget(controller: controller);
  }
}

flutter_inappwebview

webview-flutter는 필수 기능만을 제공하는 성향이 강하나 inappwebview 에서는 다양한 옵션을 제공한다. 기본적으로 webview 라고 할 수 있는 InAppWebviewWidget 외에도 총 9가지의 웹뷰가 내장되어있다고 한다. 새창열기 등도 지원해서 인기가 높으며 JavaScriptChannel을 통한 확장도 자유롭다. 다만 모든 브라우저에 일일이 대응해야한다는 단점이 있다.

6.0.0

// flutter_inappwebview 6.0.0
// 가장 기본적인 동작

// webview_flutter와 다르게 자바스크립트 사용이 기본적으로 설정되어있다.

class WebviewPage extends StatefulWidget {
  const WebviewPage({super.key});

  @override
  State<WebviewPage> createState() => _WebviewPageState();
}

class _WebviewPageState extends State<WebviewPage> {
    // inappwebview 컨트롤러 생성
  InAppWebViewController? webViewController;

    // webviewKey 생성
  final GlobalKey webViewKey = GlobalKey();

    // inappwebview의 옵션을 설정하는 부분
    // 수십개의 옵션이 있다
    // https://inappwebview.dev/docs/webview/in-app-weview-settings/
    // 위 페이지 참고 필요
  InAppWebViewSettings settings = InAppWebViewSettings(
            // 오디오 자동재생 가능 여부
      mediaPlaybackRequiresUserGesture: false,
            // 기본 컨트롤러가 아닌 브라우저 제공 컨트롤러를 활용하고 싶은 지 여부
      allowsInlineMediaPlayback: true,
            // iframe에서 사용할 수 있는 기능 추가 
      iframeAllow: "camera; microphone",
            // 전체화면 여부
      iframeAllowFullscreen: true,
            // Web Inspector에서 WebView를 검사할 수 있는지 여부
      isInspectable: kDebugMode,
    );

  @override
  Widget build(BuildContext context) {
    return InAppWebView(
            // key
      key: webViewKey,
            // 불러올 URL
      initialUrlRequest: URLRequest(
        url: WebUri("https://soye-portfolio.vercel.app/"),
      ),
            // 세팅값
      initialSettings: settings,
            // 사용할 컨트롤러
      onWebViewCreated: (controller) {
        webViewController = controller;
      },
    );
  }
}

웹과 앱, 양방향 통신

웹뷰를 활용했을 때 상단 네비게이션바와 같이 앱의 디자인 요소와는 맞지 않는 UI가 보인다거나, 앱에서 생성된 데이터를 웹으로 전달할 필요가 있을 때가 있다. 그럴 때는 웹뷰의 양방향 통신을 활용해 이를 해결할 수 있다.

양방향 통신의 방법은 webview_flutterflutter_inappwebview 모두 javascriptChannel 을 통해 시도된다.

webview_flutter로 웹뷰를 작성하기 보다는 flutter_inappwebview가 사용성이 더 낫다고 생각해 flutter_inappwebview 가이드를 먼저 올리자면,

flutter_inappwebview의 APP ↔ Web 통신

Web → App (App 코드)

@override
Widget build(BuildContext context) {
  return InAppWebView(
    key: webViewKey,
    initialUrlRequest: URLRequest(
      url: WebUri("http://10.0.2.2:3001/"),
    ),
    initialSettings: settings,
    onWebViewCreated: (controller) {
      webViewController = controller;

      // web => flutter
            // 자바스크립트 핸들러를 추가
            // handleName은 채널명으로 Web <-> App 사이의 통로이름을 정해준다고 생각하면 된다.
      controller.addJavaScriptHandler(
        handlerName: "jsChannel",
                // 전달 받은 값을 어떻게 처리할 지에 대한 콜백함수
        callback: (data) {
          print('jsChannel : recieve data ===> $data');
        },
      );
    },
  );
}

Web → App (Web 코드)

function App() {

    // window 객체에 이벤트를 추가한다.
    // 당연하게도 window 객체에는 flutterInAppWebViewPlatformReady 이라는 이벤트는 없으나
    // 웹뷰가 로드되면서 플러터가 JS에 자연스럽게 추가해주게 된다.
  window.addEventListener("flutterInAppWebViewPlatformReady", (event) => {
        // flutter_inappwebview.callHandler() 도 마찬가지인데,
        // 웹뷰가 로드되면서 추가된 이 callHandler를 통해 App과 통신하게 되며
        // 첫째 인자로는 통로이름인 채널명을 작성하고, 뒷 인자들에는 보내줄 데이터를 작성하면 된다.
    window.flutter_inappwebview.callHandler("jsChannel", "Hello World!");
  });

    return ... 컴포넌트들 ...
}

웹에서 앱으로 전달된 데이터

[AndroidInAppWebViewController] (android) WebView ID 0 calling "jsChannel" using [Hello World!] 이라는 알림을 log 창에서 확인할 수 있다.

App → Web (App 코드)

@override
Widget build(BuildContext context) {
  return InAppWebView(
    key: webViewKey,
    initialUrlRequest: URLRequest(
      url: WebUri("http://10.0.2.2:3001/"),
    ),
    initialSettings: settings,
    onWebViewCreated: (controller) {
      webViewController = controller;

        // flutter => web
            // 자바스크립트 핸들러를 추가, Web -> App 과 동일
      controller.addJavaScriptHandler(
                // 통신할 채널명
        handlerName: 'flutterChannel',
                // return을 통해 값을 앱 => 웹으로 전달해줌 (이부분이 차이점)
        callback: (args) {
          return true;
        },
      );
    },
  );
}

App → Web (Web 코드)

function App() {
    const [isFlutter, setIsFlutter] = useState(false);

    // 기본적으로 Web -> App과 동일하지만
    // .then 구문을 통해 앱으로부터 전달받은 전달 값을 활용한다.
    window.addEventListener("flutterInAppWebViewPlatformReady", function (event) {
      window.flutter_inappwebview
        .callHandler("flutterChannel")
        .then(function (result) {
          setIsFlutter(result);
        });
    });

    return ... 컴포넌트들 ...
}

앱에서 웹으로 전달한 데이터로 만든 결과물

위와 같이 App ↔ Web 간 통신을 활용하게 되면 웹으로 제작된 사이트도 앱으로서의 역할을 할 수 있게 만들어주는 하이브리드 앱이 만들어진다.

웹뷰의 장점이라고 한다면 이런 하이브리드앱으로의 구성을 통해서 가령 네이티브 기능인 카메라와 같은 QR코드를 찍는 페이지는 네이티브로 만들고 이에 대해 받아진 데이터를 웹으로 전달해주거나, 생체인식 로그인과 같은 로그인 기능도 인증이 됐을 때 토큰을 웹 또는 서버로 넘겨준다거나 하는 형태로 사용이 가능하다.

마치며…

아직 활용가능한 것 혹은 이해한 것들이 그리 깊지도 많지도 않지만 개발자 1인의 스타트업에서 웹뷰의 존재는 굉장히 감사한 것 아닌가 싶다. 사용하다보면 모두와 같이 웹뷰에 대한 욕을 하고 있을 수도 있겠으나, 결국 더 나은 서비스를 제공하기 위함인 만큼 깊게 공부하고 최대한 네이티브 앱처럼 동작할 수 있도록 해야겠다.

p.s. 네이티브앱이라는 단어를 혼용해서 쓰고 있긴한데, 그냥 이 포스팅에서는 웹뷰로 작성된 것이 아닌 APP 그 자체를 위한 개발이라고 대충 생각하고 읽어주자..!

0
Subscribe to my newsletter

Read articles from 개똥이 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

개똥이
개똥이

어제보다 더 나은 서비스를 만들어내는 사람이 되고자 노력하며, 내일의 나를 위해 기록합니다.