떨리다로 웹앱 만들며 진행했던 정리 (1)


서론flutter 2.0이 발표 되었다는 소식을 듣고 그동안 관심을 많이 갖고 있었던 flutter 와 친해져보기 위해서 관련해서 공부했던 내용들을 정리해보려고 합니다. 아무래도 FE개발을 하다보니 웹앱에 관심이 가장 많았었고 hybrid web app 으로서는 어떨지 이것저것 테스트 해봤던 내용들을 공유해보려고 합니다.

떨리다flutter 는 구글에서 출시한 크로스 플랫폼 SDK입니다. 2에서는 android/iOS/web 을 지원합니다.(베타)는 뺐습니다) 흥미가 많이 끌렸던 이유는 과거 electron 으로 앱을 개발할때와 같은 단일 코드 베이스에 매력이 제일 컸던것 같습니다. flutter 가 무엇이고 어떻게 렌더링을 한다등의 내용은 지금의 주제에서 벗어나는것 같아서 추후 정리하려 하고, 오늘 정리는 오로지 카카오톡 로그인 붙여보기에 목적이 있습니다.

네트워크 보기
네트워크 보기는 플러그 인으로 공식 지원하는 webview_flutter 와 개인 개발자가 만든 flutter_inappwebview 가 있습니다. webview_flutter 는 그물 모양의 물건과 메세지를 주고받을 수 있는 javascriptChannelsandroidshouldOverrideUrlLoading 과 유사한 역할을 하는 navigationDelegate 와 같은 인터페이스가 있습니다. 처음 검토했을때는 컴팩트하게 꼭 필요한 기능만 갖춘 느낌이었고 flutter_inappwebview 는 굉장히 파워풀하게 인터페이스들이 많아서 flutter_inappwebview 를 선택해서 개발했었습니다.

카카오 로그인
카카오 로그인을 혼합 응용으로 구현할때는 카카오 로그인 하이브리드 앱에 적용하기 에 있는 내용들이 적용이 되어야합니다. flutter 에는 MethodChannel 이란 사용자 인터페이스와 진행자(플랫폼)간의 메세지를 보낼수 있는 인터페이스가 있습니다. 이것을 보고 처음 구상했던 방법은 아래와 같습니다.

MethodChannel+는 다음을 덮어써야 합니다.

매인.던지다
// main.dart
// MethodChannel 객체 생성
static const platform = const MethodChannel('intent');

// inappwebview의 shouldOverrideUrlLoading 사용

...
// InAppWebView 컴포넌트 내
shouldOverrideUrlLoading:
    (controller, NavigationAction navigationAction) async {
  var uri = navigationAction.request.url!;
  if (uri.scheme == 'intent') {
    try {
      var result = await platform
          .invokeMethod('launchKakaoTalk', {'url': uri.toString()});
      if (result != null) {
        await webViewController?.loadUrl(
            urlRequest: URLRequest(url: Uri.parse(result)));
      }

    } catch (e) {
      print('url fail $e');
    }
    return NavigationActionPolicy.CANCEL;
  }
  return NavigationActionPolicy.ALLOW;
},
...

주요 활동.kt
class MainActivity: FlutterActivity() {
    private var CHANNEL = "intent"
    private var methodChannel: MethodChannel? = null

    @SuppressLint("NewApi")
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL);
        methodChannel?.setMethodCallHandler { call, result ->
            if (call.method == "launchKakaoTalk") {
                var url = call.argument<String>("url");
                val intent = Intent.parseUri(url, URI_INTENT_SCHEME);
                // 실행 가능한 앱이 있으면 앱 실행
                if (intent.resolveActivity(packageManager) != null) {
                    val existPackage = packageManager.getLaunchIntentForPackage("" + intent.getPackage());
                    startActivity(intent)
                    result.success(null);
                } else {
                    // Fallback URL이 있으면 현재 웹뷰에 로딩
                    val fallbackUrl = intent.getStringExtra("browser_fallback_url")
                    if (fallbackUrl != null) {
                        result.success(fallbackUrl);
                    }
                }
            } else {
                result.notImplemented()
            }
        }
    }
}
관련 내용을 찾아보다가 카드 시험에서 공식적으로 제공하는 kakao_flutter_sdk 의 존재도 알게 되었습니다. 내부 코드를 살펴보니 패키지 검사를 아래처럼 하고있었습니다.
fun isKakaoTalkInstalled(context: Context): Boolean {
  return isPackageInstalled(context, "com.kakao.talk") || isPackageInstalled(context, "com.kakao.onetalk")
}

private fun isPackageInstalled(context: Context, packageName: String): Boolean {
  return context.packageManager.getLaunchIntentForPackage(packageName) != null
}
네트워크 운영 체제는 아래와 같이 플러그 인을 생성해서 MethodChannel 을 연결해줘야합니다. 네트워크 운영 체제용 plist 설정 과 같은 카카오로그인 개발 환경 설정도 다 해주어야합니다.
아래와 같은식으로 개발을 진행했습니다. (동작은 확인했지만 개발하다 중지한 코드입니다)

WebviewPlugin.날래다

import Foundation

public class WebviewPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "intent", binaryMessenger: registrar.messenger())
    let instance = WebviewPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    print(call);
    switch call.method {
      case "launchKakaoTalk":
        guard let talkUrl = URL(string: "kakaokompassauth://authorize") else {
          result(false);
          return
        }

        if (UIApplication.shared.canOpenURL(talkUrl)) {
          let args = call.arguments as! Dictionary<String, String>
          let uri = args["url"]
          launchKakaoTalk(uri: uri!, result: result)
        }
    default:
      result(FlutterMethodNotImplemented)
    }    
  }

  private func launchKakaoTalk(uri: String, result: @escaping FlutterResult) {
    let urlObject = URL(string: uri)!
    // 주의: 아래부분처럼 urlObject를 kakaokompassauth 로 시작되는 URL로  파싱해줘야합니다. 이 코드는 변경하다만 코드입니다.
    if (UIApplication.shared.canOpenURL(urlObject)) {      
      let url = URL(string: "kakaokompassauth://authorize?redirect_uri=kakaod{id}://oauth&response_type=code&client_id={client_id}7")!;
      UIApplication.shared.open(url, options: [:]) { (openResult) in
        result(openResult)
      }
    }
  }
}

생각의 전환kakao_flutter_sdk 를 전체적으로 분석하고 난 이후 여러 생각이 들었습니다. 실제로 iOS 네이티브 코드로 카카오 로그인을 구현해본적도 없었고 때문에 각 플랫폼별로 신경써야하는 부분도 잘몰랐었습니다. 각 플랫폼 별로 카카오톡이 설치된지에 대한 판단 방법도 다르다던지, 카카오톡 호출 방법 역시 플랫폼별로 처리해줘야하는 방식이 달랐습니다. (여담이지만 네트워크 운영 체제에서 카카오톡 앱을 호출할때는 kakaokompassauth 이런 형식의 스키마를 호출해야한다는 것도 이번에 알았습니다)
차라리 웹에서 flutter 로 호출된 페이지라면 javascriptChannel 을 통해서 kakao_flutter_sdk 의 메소드를 호출을 하면 안될까? 플랫폼별 카카오톡 호출 방식, 플랫폼별 사용자 인터페이스를 고려한 동작들이 구현된 내용을 이해하고 사용하는게 오히려 나은것 아닐까? 라는 생각이 뇌리를 스쳐서 구현 방식을 변경해봤습니다.
kakao_flutter_sdk 의 문서에 나와있는 초기 설정은 다 해줘야합니다. (카드 개발 업체)의 환경설정에 있는 내용들도 있습니다)

매인.던지다
...
// InAppWebView 컴포넌트 내부
onWebViewCreated: (InAppWebViewController controller) {
  ...
  webViewController?.addJavaScriptHandler(
    handlerName: 'loginKakao',
    callback: (arguments) async {
      try {
        final installed = await isKakaoTalkInstalled();
        final authCode = installed
            ? await AuthCodeClient.instance.requestWithTalk()
            : await AuthCodeClient.instance.request();
        return authCode;
      } on KakaoAuthException catch (e) {
        return null;
      } on Exception catch (e) {
        return null;
      }
    });
  ...
}

Login 구성 요소tsx
const REDIRECT_URI = '어딘가';

const doKakaoLogin = () => {
  // 현재 webview인지 판단
  if (isWebview()) {
    window.flutter_inappwebview
      .callHandler('loginKakao')
      .then(async function (authCode: string) {
      // authCode를 이용해서 인증처리.
    }
  } else {
    Kakao.Auth.authorize({
        redirectUri: REDIRECT_URI,
        // 그 외 필요한 내용.
      });
  }
}

ㅇ_ㅇ…
끝.
ㅇ ㅏ… 물론 웹뷰에서 받았던 유니버셜링크를 그대로 활용할수있는 방법도 더 찾아보고싶었는데 세세하게 수정할 구간들이 많아서 나중에 더 연구해보고싶긴 하네요..

마무리hybrid web app 환경에서 카카오톡 로그인을 던지다와 네이티브 코드를 직접 구현해보니 네이티브로까지의 전체적인 흐름이나 여기서 다루지 못한 kakao_flutter_sdk 에서의 사용자 인터페이스를 신경쓴 플로우 등을 보면서 많은 공부가 되었습니다. 결국 카카오 로그인을 구현하려고 보니 고민하고 신경써야하는 부분들이 플러그인에 세세하게 잘 구현이 되어있어서 플러그인을 사용하기 전에 구현해보았던 동작들이 잘 이해가 되었고 위와같이 구현해보았습니다. 정리가 한번 더 되면 직접 만들어서 사용해보자가 다음 아이템이 될수도 있을거같기도 하네요.
정리를 좀 하고 작성하고 싶었는데 누락된 구간이 있을수도 있어서 검증 해보고 문서를 수정..할수도 있을것 같습니다 (뭔가 급하게 쓴 느낌이라)
감사합니다.

좋은 웹페이지 즐겨찾기