Flutter로 웹앱 만들며 진행했던 정리 (1)
Tags: flutter, kakaologin, webview, hybrid
Published: 2021-04-14 15:47:42


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

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

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

## 카카오 로그인
---
카카오 로그인을 hybrid app으로 구현할때는 [카카오 로그인 하이브리드 앱에 적용하기](https://developers.kakao.com/docs/latest/ko/getting-started/sdk-js)에 있는 내용들이 적용이 되어야합니다. `flutter`에는 [MethodChannel](https://flutter.dev/docs/development/platform-integration/platform-channels)이란 UI와 Host (platform) 간의  메세지를 보낼수 있는 인터페이스가 있습니다. 이것을 보고 처음 구상했던 방법은 아래와 같습니다.

### MethodChannel + shouldOverrideUrlLoading

#### main.dart

```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;
},
...
```

#### MainActivity.kt
```kotlin
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에서 공식적으로 제공하는 [kakao_flutter_sdk](https://pub.dev/packages/kakao_flutter_sdk)의 존재도 알게 되었습니다. 내부 코드를 살펴보니 패키지 검사를 아래처럼 하고있었습니다.

```kotlin
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
}
```

iOS는 아래와 같이 plugin을 생성해서 `MethodChannel`을 연결해줘야합니다. iOS용 [plist 설정](https://developers.kakao.com/docs/latest/ko/getting-started/sdk-ios#set-plist)과 같은 카카오로그인 개발 환경 설정도 다 해주어야합니다. 

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

#### WebviewPlugin.swift

```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)
      }
    }
  }
}
```

### 생각의 전환

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

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

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

#### main.dart
```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;
      }
    });
  ...
}
```

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

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

감사합니다.