DEV Community

Park Je Hoon
Park Je Hoon

Posted on

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

서론


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

Flutter


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

Webview


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

카카오 로그인


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

MethodChannel + shouldOverrideUrlLoading

main.dart

// 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;
},
...
Enter fullscreen mode Exit fullscreen mode

MainActivity.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()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

관련 내용을 찾아보다가 KAKAO에서 공식적으로 제공하는 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
}
Enter fullscreen mode Exit fullscreen mode

iOS는 아래와 같이 plugin을 생성해서 MethodChannel을 연결해줘야합니다. iOS용 plist 설정과 같은 카카오로그인 개발 환경 설정도 다 해주어야합니다.

아래와 같은식으로 개발을 진행했습니다. (동작은 확인했지만 개발하다 중지한 코드입니다)

WebviewPlugin.swift


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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

생각의 전환

kakao_flutter_sdk를 전체적으로 분석하고 난 이후 여러 생각이 들었습니다. 실제로 iOS 네이티브 코드로 카카오 로그인을 구현해본적도 없었고 때문에 각 플랫폼별로 신경써야하는 부분도 잘몰랐었습니다. 각 플랫폼 별로 카카오톡이 설치된지에 대한 판단 방법도 다르다던지, 카카오톡 호출 방법 역시 플랫폼별로 처리해줘야하는 방식이 달랐습니다. (여담이지만 iOS에서 카카오톡 앱을 호출할때는 kakaokompassauth 이런 형식의 스키마를 호출해야한다는 것도 이번에 알았습니다)

차라리 웹에서 flutter로 호출된 페이지라면 javascriptChannel을 통해서 kakao_flutter_sdk의 메소드를 호출을 하면 안될까? 플랫폼별 카카오톡 호출 방식, 플랫폼별 UI를 고려한 동작들이 구현된 내용을 이해하고 사용하는게 오히려 나은것 아닐까? 라는 생각이 뇌리를 스쳐서 구현 방식을 변경해봤습니다.

kakao_flutter_sdk 의 문서에 나와있는 초기 설정은 다 해줘야합니다. (kakao developers의 환경설정에 있는 내용들도 있습니다)

main.dart

...
// 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;
      }
    });
  ...
}
Enter fullscreen mode Exit fullscreen mode

LoginComponent.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,
        // 그 외 필요한 내용.
      });
  }
}

Enter fullscreen mode Exit fullscreen mode

ㅇ_ㅇ…

끝.

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

마무리


hybrid web app 환경에서 카카오톡 로그인을 dart와 네이티브 코드를 직접 구현해보니 네이티브로까지의 전체적인 흐름이나 여기서 다루지 못한 kakao_flutter_sdk에서의 UI를 신경쓴 플로우 등을 보면서 많은 공부가 되었습니다. 결국 카카오 로그인을 구현하려고 보니 고민하고 신경써야하는 부분들이 플러그인에 세세하게 잘 구현이 되어있어서 플러그인을 사용하기 전에 구현해보았던 동작들이 잘 이해가 되었고 위와같이 구현해보았습니다. 정리가 한번 더 되면 직접 만들어서 사용해보자가 다음 아이템이 될수도 있을거같기도 하네요.

정리를 좀 하고 작성하고 싶었는데 누락된 구간이 있을수도 있어서 검증 해보고 문서를 수정..할수도 있을것 같습니다 (뭔가 급하게 쓴 느낌이라)

감사합니다.

Top comments (2)

Collapse
 
pablonax profile image
Pablo Discobar

If you are interested in this, you can also look at my article about Flutter templates. I made it easier for you and compared the free and paid Flutter templates. I'm sure you'll find something useful there, too. - dev.to/pablonax/free-vs-paid-flutt...

Collapse
 
feverahn profile image
feverAhn

안녕하세요~ 개발자님 kakaolink 웹뷰 제작하다가 막혔는데

혹시 지금 작성해주신 내용 소스 공유 가능할까요?

작성해주신 내용대로 적용 해봤는데도 여전히 안되는데 뭐가 문제인지 모르겠습니다;;