Flutter WebView로 PG 결제 연동 뚫어내기 (SDK 없을 때의 대처법)

모바일 앱에 결제 기능을 추가할 때, PG(Payment Gateway)사가 제공하는 네이티브 SDK를 사용하는 것이 가장 일반적입니다. 하지만 프로젝트 요구사항이나 특정 PG사의 정책으로 인해 Flutter 전용 SDK가 없는 난감한 상황에 직면하기도 합니다. 이 글에서는 Flutter SDK 없이 웹뷰(WebView)를 활용하여 국내 PG사 결제 연동을 성공적으로 구현한 경험과, 그 과정에서 마주한 기술적 난관들을 해결한 방법을 상세히 공유합니다.

Deep Dive: 왜 웹뷰 결제는 항상 실패하는가?

웹 개발 환경에서 잘 작동하던 결제 모듈을 앱의 웹뷰에 띄우면, 십중팔구 결제 버튼을 누르는 순간 "페이지를 찾을 수 없습니다" 혹은 "알 수 없는 프로토콜" 에러를 뱉으며 크래시가 납니다. 이것은 Flutter나 WebView의 버그가 아닙니다.

국내 PG사(KG이니시스, 토스, 나이스페이 등)의 결제 로직은 http/https 프로토콜 외에도 각 카드사의 앱(ISP, 앱카드)을 호출하기 위해 Custom URL Scheme(예: ispmobile://)이나 Android의 Intent Scheme(예: intent://)을 사용하기 때문입니다.

핵심 문제: 기본 WebView는 http/s 외의 프로토콜을 처리할 줄 모릅니다. intent://로 시작하는 요청이 들어오면 웹뷰는 이를 "탐색해야 할 웹 주소"로 인식하고 로딩을 시도하다 에러를 발생시킵니다.

따라서 우리는 웹뷰가 요청하는 URL을 가로채서(Intercept), 이것이 웹 페이지 이동인지 아니면 외부 앱(카드사 앱) 실행 요청인지를 판단하고 직접 처리해줘야 합니다. Android Intents 메커니즘을 이해해야만 이 문제를 해결할 수 있습니다.

The Solution: NavigationDelegate를 이용한 스킴 가로채기

가장 널리 쓰이는 webview_flutter 패키지를 기준으로 설명합니다. 핵심은 NavigationDelegateonNavigationRequest 콜백입니다. 여기서 URL을 검사하여 로직을 분기합니다.

Note: Android의 경우 intent:// 스킴을 파싱하여 앱이 설치되어 있지 않을 때 마켓으로 이동시키는 로직이 필수적입니다.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart'; // 외부 앱 실행을 위해 필수
import 'dart:io';

class PaymentWebView extends StatefulWidget {
  final String paymentUrl;

  const PaymentWebView({Key? key, required this.paymentUrl}) : super(key: key);

  @override
  State<PaymentWebView> createState() => _PaymentWebViewState();
}

class _PaymentWebViewState extends State<PaymentWebView> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    
    // WebViewController 초기화 (webview_flutter 4.x 이상)
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (NavigationRequest request) async {
            final url = request.url;
            
            // 1. http/https 스킴은 정상적으로 웹뷰 내에서 로딩
            if (url.startsWith('http://') || url.startsWith('https://')) {
              return NavigationDecision.navigate;
            }

            // 2. 그 외(Intent, Custom Scheme)는 외부 앱 실행 시도
            if (await canLaunchUrl(Uri.parse(url))) {
              await launchUrl(
                Uri.parse(url),
                mode: LaunchMode.externalApplication,
              );
              return NavigationDecision.prevent; // 웹뷰 이동 막기
            } else {
              // 3. Android Intent 처리 (앱 미설치 시 마켓 이동 등)
              if (Platform.isAndroid && url.startsWith('intent://')) {
                try {
                  // Intent URL 파싱 로직 (간소화됨)
                  // 실제로는 'browser_fallback_url'이나 'package' 파라미터를 파싱해야 함
                  await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
                } catch (e) {
                  // Fallback: 파싱 실패 시 처리 (예: 로그 출력)
                  debugPrint("Failed to launch intent: $e");
                }
                return NavigationDecision.prevent;
              }
              
              debugPrint("Cannot launch url: $url");
              return NavigationDecision.prevent;
            }
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.paymentUrl));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("PG 결제")),
      body: WebViewWidget(controller: _controller),
    );
  }
}

필수: 플랫폼별 설정 (이거 안 하면 무조건 터짐)

코드를 짰다고 끝이 아닙니다. Android와 iOS 보안 정책에 따라 외부 앱을 실행하려면 권한 명시가 필요합니다.

Platform File Configuration
Android AndroidManifest.xml <queries> 태그에 카드사/은행 패키지 명시 필요 (Android 11+)
iOS Info.plist LSApplicationQueriesSchemes 키에 URL 스킴(ispmobile, kakaotalk 등) 등록

iOS의 경우 Info.plist에 아래와 같이 주요 카드사 스킴을 모두 등록해야 canLaunchUrltrue를 반환합니다. 하나라도 빠지면 해당 카드 결제 시 아무 반응이 없습니다.

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>kftc-bankpay</string> <!-- 계좌이체 -->
    <string>ispmobile</string> <!-- ISP모바일 -->
    <string>itms-apps</string> <!-- 앱스토어 -->
    <string>kb-acp</string> <!-- KB국민카드 -->
    <string>mpocket.online.ansimclick</string> <!-- 삼성카드 -->
    <string>hdcardappcardansimclick</string> <!-- 현대카드 -->
    <!-- 필요한 스킴 추가 -->
</array>

Conclusion

Flutter용 공식 SDK가 없다고 해서 웹뷰 연동을 두려워할 필요는 없습니다. 본질적으로 PG 결제창은 웹페이지이며, 모바일 환경에서의 URL Scheme 핸들링만 정확하게 구현한다면 네이티브 못지않은 결제 경험을 제공할 수 있습니다. 위 코드는 기본적인 뼈대이며, 실제 프로덕션 환경에서는 ISP 앱이 설치되지 않았을 때 마켓으로 이동시키는 fallback 로직을 좀 더 정교하게 다듬어야 합니다. 국내 PG 연동의 핵심은 "웹뷰가 모르는 주소는 OS에게 맡긴다"는 원칙만 기억하면 됩니다.

Post a Comment