최근 신규 프로젝트에서 Flutter로 구현된 익명 채팅방 기능을 네이티브 프로젝트에 연결해서 사용해야하는 작업을 진행하게 되었다. 본인도 Flutter 모듈과 연결해보는 것이 처음이었고, Flutter 개발자도 이전에 Android 개발자로서 이번 프로젝트 때 처음으로 Flutter를 작업해본 지라 많은 시행착오를 겪고서 겨우 연결할 수 있었다.
이와 관련한 정보도 생각 외로 적다보니 혹시 모를 도움이 되지 않을까 싶어 관련된 내용을 포스트로 남기고자 생각하였다.
가장 먼저 해야될 일은 전달받은 Flutter 모듈을 Pod으로 연결하여 Flutter 프레임워크를 import하여 사용할 수 있도록 하는 것이었다.
일단 전달받은 Flutter 모듈 디렉토리는 네이티브 프로젝트의 디렉토리와 동일 선상에 두어야 한다.
위의 사진과 같이 기존 프로젝트의 디렉토리와 Flutter 모듈의 디렉토리는 동일한 상위 디렉토리 내에 위치시켜 놓으면 된다.
이제는 기존 네이티브 프로젝트에 생성되어 있던 Podfile 내에 Flutter 모듈의 경로를 설정하고 install할 수 있도록 몇 가지의 내용을 작성해주어야 한다.
flutter_application_path = '../chat_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
target 'Project' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for Project
# Flutter
install_all_flutter_pods(flutter_application_path)
end
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
이렇게 내용을 추가하고 pod install
혹은 pod update
를 하기만하면 바로 설치!가 되지 않았다. 공식 자료를 포함한 대부분의 자료에서는 본인이 직접 모듈을 만든 상황을 가정하여 설치법을 알려주다보니 본인처럼 다른 작업자에게 모듈을 받아온 상황에 대한 내용이 없어 pod을 설치하는 과정에서부터 막혀버리게 되었다.
이와 같이 타인에게 Module을 받아온 터라 설치를 위해 필요한 podhelper
라는 것의 경로가 본인이 설정한 경로와 맞지 않아 에러가 뜨게 된다. 그래서 처음에는 Flutter 모듈 내의 .iOS(숨긴 파일)/Flutter 디렉토리 내에 있는 flutter_export_environment.sh
와 Generated.xcconfig
의 Root와 Path 값을 강제로 수정해서 다시 시도해보았다.
그 결과는 당연히 대실패. 분명히 generated된 파일이라 수정하지 말라는 문구가 있음에도 강제로 수정한 자의 비참한 말로였다. 그러던 중, flutter의 명령어 중에 기존에 generated된 내용을 삭제하는 clean
과 다시 generate하는 pub get
을 알게 되었고 이를 통해 본인의 경로로 다시 generate해보기로 생각했다.
위의 명령어로 새롭게 Flutter 모듈을 generate한 뒤, 다시 네이티브 프로젝트로 돌아와 pod update
를 하니 별다른 오류가 없이 설치가 진행되었다. 결국 타인의 경로에 맞춰 generate되었던 모듈을 본인의 경로에 맞춰서 install하려다보니 생긴 문제였다.
이러한 시행착오 끝에 Flutter의 모듈을 네이티브 프로젝트에 import하여 사용할 수 있게 되었다.
모든 설정이 끝났으니 이제 Flutter 모듈을 import하여 원하는 View를 보여주게끔 설정해주어야 한다. 먼저 AppDelegate
를 FlutterAppDelegate
로 바꿔주고, 원하는 View를 보여주기 위한 Engine을 구현하고자 했다.
import UIKit
import Flutter
import FlutterPluginRegistrant
@main
class AppDelegate: FlutterAppDelegate {
private let flutterGroup: FlutterEngineGroup = .init(name: "chat_module", project: nil)
private(set) var loginEngine: FlutterEngine?
private(set) var aiEngine: FlutterEngine?
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
PushLocationManager.shared.configureFirebase()
loginEngine = flutterGroup.makeEngine(withEntrypoint: nil, libraryURI: nil, initialRoute: "/login")
aiEngine = flutterGroup.makeEngine(withEntrypoint: nil, libraryURI: nil, initialRoute: "/ai")
guard let loginEngine, let aiEngine else {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
GeneratedPluginRegistrant.register(with: loginEngine)
GeneratedPluginRegistrant.register(with: aiEngine)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
본인의 경우에는 보여줄 화면이 두 개인지라 각각의 엔진을 만들고 이를 Group으로 묶었기에 위와 같이 FlutterEngineGroup
과 makeEngine(withEntrypoint:, libraryURI:, initialRoute:)
을 사용하여 생성해주었다.
특히 makeEngine
메서드를 사용했을 때, initialRoute
를 지정해주었는데 이는 멀티 페이지 이동을 할때, 화면에 제일 처음 출력되는 라우트를 불러오는 역할을 해주며 Flutter에서 해당 라우트를 지정해주어야지만 정상적으로 원하는 페이지를 불러올 수 있다.
이와 함께 FlutterAppDelegate
를 상속받았을 때, connectingSceneSession
을 오버라이드할 수가 없다보니 몇 가지 문제가 발생하였는데 해당 메서드는 작성하지 않아도 Scene을 불러오는데 문제가 없으니 시원하게 제거해주면 된다.
이제 원하는 VC로 가서 FlutterViewController
를 생성하고 해당 화면으로 이동만 해주면 연결 작업이 끝나게 된다.
guard let loginEngine = (UIApplication.shared.delegate as? AppDelegate)?.loginEngine else { return }
let flutterVC: FlutterViewController = .init(engine: loginEngine, nibName: nil, bundle: nil)
let loginChannel: FlutterMethodChannel = .init(name: self.channelName, binaryMessenger: flutterVC.binaryMessenger)
loginChannel.invokeMethod("getUserToken", arguments: token) { result in
owner.coordinateDelegate.pushFlutter(with: flutterVC)
}
위와 같이 AppDelegate
에서 구현한 엔진을 통해 FlutterViewController
를 생성만 해주면 된다. 혹여 본인처럼 FlutterViewController
에 전달해야하는 요소가 있거나 네이티브의 고유 기능 등을 사용해야될 필요가 있다면 FlutterMethodChannel
을 생성하고 Flutter의 메서드명에 맞춰 invokeMethod
를 호출해주면 된다.
이렇게 정리를 하다보니 별 게 아닌 것 같았는데 처음으로 연결 작업을 진행하다보니 생각 외로 많은 우여곡절을 겪게 되었다. 크로스 플랫폼이 각광을 받으면서 단순히 네이티브 작업 외에도 신경 써야되는 부분들이 많아진다는 걸 뼈저리게 느낀 작업이었다.
추가적으로 Debug 상태로 Run을 하게 되면 Xcode로 Run한 상태에서는 문제 없이 잘 작동하지만, 실기기에서 앱을 눌러 실행할 경우에는 전혀 작동하지 않는 경우가 있다. 이 경우에는 Release로 바꿔서 Run을 한 뒤에 다시 앱을 눌러 실행하면 잘 작동하니 이런 상황을 맞닥뜨린 경우에는 한 번 위와 같은 방법을 시도해보기 바란다.