fluter_app_badger | Flutter Package
firebase_messaging | Flutter Package
플랫폼 통신(IOS) - Method Channel
플랫폼 통신(IOS) - Event Channel
플랫폼 통신(Android) - Method Channel
플랫폼 통신(Android) - Event Channel
Lifecycle(앱 상태) 네이티브에서 확인하기 - IOS
Lifecycle(앱 상태) 네이티브에서 확인하기 - AOS
이번 글에서는 Remote Push를 수신 받아 앱 아이콘 뱃지의 카운트를 표시하는 방법에 대해서 설명하도록 하겠다.
조금은 어려운 내용일 수 있지만 최대한 자세하게 앱 아이콘 뱃지를 처리하는 방법에 대해서 다뤄볼 예정이다. 먼저 해당 기능은 IOS 환경에서만 사용되는 기능이고, Android 경우에는 처리하지 않아도 시스템이 알아서 알림 카운트를 관리해준다.
이미 Remote Push 관련 설정은 완료되었다는 가정하에 오로지 앱 아이콘 뱃지를 처리하는 방법만 다룰 것이고, Swift 코드를 사용하기도 하며, IOS 플랫폼 상태에 대한 이해도도 조금은 필요할 수도 있다.
몇 달전에 앱 아이콘 뱃지를 처리해야 하는 경우가 있어서, 작성한 글이 있었는데, 해당 글에서 다뤘던 내용에 잘못된 내용도 많고 너무 간단하게 작성한 것 같아 다시 작성하기로 하였다. 이전 글이 궁금하신 분들은 위에 공유된 링크 참고하길 바라며, 추가적으로 네이티브 플랫폼과 Flutter 간의 통신인 Platform Channel 관련한 글과 네이티브 환경에서 앱 상태를 체크하는 생명주기(Life Cycle)관련 글도 함꼐 공유하였습니다.
위에 공유된 기능들이 이번 기능을 만드는데, 전부 사용되는 기능들입니다.
사실 대부분의 서비스는 앱 아이콘 뱃지를 서버에서 관리해준다... 클라이언트에서 처리하기에는 복잡도도 있고, 제대로 처리가 되지않는 이슈가 많아서 서버에서 하는게 일반적인데, 저처럼 클라이언트 단에서 처리해야 한다면 공유하는 글을 잘 참고하시기 바란다.
Flutter에서 App Icon Badge를 처리하는 방법은 flutter_app_badger라는 라이브러리를 사용하여 뱃지 카운트를 관리하는 방법이 있다.
하지만 문제는 앱의 상태에서 Remote Push를 수신해야 아이콘 뱃지의 카운트를 증가시키던 초기화시키던 할텐데, 푸시 수신이 Flutter에서는 불가능하다.
firebase_messaging 라이브러리에서 제공하는 onBackgroundMessage 함수가 있는데, 이 기능도 background에서만 가능하지 앱 종료 상태에서는 작동하지 않는다.
Flutter에서 앱 아이콘 뱃지를 원활히 처리하기 위해서는 리스너가 필요하다. 먼저 리스너는 Flutter Life Cycle인 Foreground/Background/Terminated 각 상태에 맞는 리스너가 제대로 작동이 되어야 앱 아이콘을 처리할 수 있다.
여기서는 리스너 부분외에도 앱 아이콘 뱃지 카운트 관리를 네이티브에서 받아오는 방법과 앱의 생명주기를 활용해서 Flutter와 IOS 간의 생명주기 차이에 따른 이슈들에 대해서 해결 방법을 제시하도록 하겠다.
Flutter에서는 Remote Push가 시스템에 노출되는 시점에 푸시 정보를 수신할 수 없다. 물론 Foreground/background 환경에서는 수신이 가능하기는 하나, 정작 앱 종료 상태에서 수신을 받지 못하면 아무 의미가 없다.
firebase_messaging 라이브러리에서 제공하는 기능을 사용하면 백그라운드와 포어그라운드 수신이 가능하기는 하다.
FirebaseMessaging.onBackgroundMessage((message) => null);
FirebaseMessaging.onMessage.asBroadcastStream();
앱 종료 상태에서 푸시를 수신 받기 위해 네이티브 플랫폼 환경에서 받아오고자 한다.
IOS에서는 Push가 수신될 때에 didReceiveRemoteNotification에서 푸시 정보를 수신 받을 수 있도록 해준다. 우리도 해당 기능을 활용할 예정이다.
didReceiveRemoteNotification은 swift 개발시에는 푸시 배너를 클릭했을 때에 호출이 된다고 하는데, 제가 개발했을 때 Flutter에서는 푸시가 들어오는 시점에만 호출이 된다.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
...
}
위에서 작성한 코드에 라이프 사이클을 넣어서 분기 처리를 해보자. 아래와 같이 백그라운드, 종료, 실행 중 상태에 따라 푸시를 수신받을 수 있다.
firebase_massaging 라이브러리에 있는 onMessage 기능이 Foreground 환경에서 푸시를 수신 받도록 해주는 기능인데, onMessage를 사용하지 않고 아래의 네이티브에서 처리해도 된다.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let state = UIApplication.shared.applicationState
if state == .background {
//
} else if (state == .active) {
//
} else if (state == .inactive) {
//
}
}
위에서 작성한 상태에 따라 제대로 수신을 받을 수 있는지 Xcode를 열어 로그를 찍어서 푸시를 보내보자.
background / active 에서 제대로 로그가 찍히는 것을 확인할 수 있다. 하지만 앱 종료 상태인 inactive에서 로그가 제대로 출력이 되는지는 확인할 수가 없다. inactive 상태에서 테스트는 잠시 미뤄두자.
만일 위에서 로그가 출력이 되지 않았다면 푸시의 ios payload를 제대로 입력했는지 확인이 필요하다.
페이로드에 반드시 포함되어야 하는 내용이다. 해당 값이 없으면 푸시 수신이 되지 않는다.
content-available : 1
hearders : {
"apns-push-type" : "background",
"apns-priority" : 5
}
여기까지 잘 따라왔다면 이제 본격적인 테스트를 해보자. swift는 네이티브이기 때문에 현재의 아이콘 뱃지 갯수를 가져올 수가 있다. 엄청 좋은 기능이다. Flutter에는 없어요..
아래 코드를 보면 UIApplication에서 현재의 뱃지 넘버를 가져올 수 있기 때문에, 증감연산자로 코드를 수정해서 다시 테스트 해보자. 아래 코드는 현재의 뱃지 넘버를 리턴하기도 하지만 변경도 할 수있다. 자세한건 아래에서 확인하도록 하겠다.
UIApplication.shared.applicationIconBadgeNumber
아래와 같이 코드를 수정해주고, 앱 종료 상태 테스트를 위해 릴리즈 빌드를 진행하자.
이제 푸시를 다시 보내보면 카운트 값이 어떻게 변화는지 상태에 따라 디버깅을 할 수있다. 앱이 종료된 상태에서 inactive가 정상적으로 호출이 되면 현재 앱 아이콘 뱃지에 3을 더한 값으로 뱃지 값을 변경할 것이다.
여기서 문제가 발생했다. 저같은 경우에는 1~2번 정도는 3씩 증감을 하다가 다시 1씩 증감을 하고있다. 여기서 플랫폼 간의 라이프 사이클 차이가 발생한다는 것을 알 수 있는데, Flutter에서는 inactive가 호출되지 않는다. Flutter 플랫폼의 FlutterViewController가 활성화된 상태에서 해당 applciation의 상태를 리턴하기에 항상 백그라운드로 인식이 되는 것이다.
만일 지속적인 테스트에도 inactive 상태가 잘 수신된다면 문제가 없다.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let state = UIApplication.shared.applicationState
if state == .background {
UIApplication.shared.applicationIconBadgeNumber += 1
} else if (state == .active) {
UIApplication.shared.applicationIconBadgeNumber += 2
} else if (state == .inactive) {
UIApplication.shared.applicationIconBadgeNumber += 3
}
}
근데 왜 상태에 따른 분기를 했을까 ? 그냥 단순히 어떤 상태에서 푸시가 수신되면 1을 증감하면 되는거 아닌가 ? 라고 생각할 수도 있다.
앱을 종료한 상태로 푸시를 보내보자. 아이콘 뱃지 카운트가 올라갔을 것이다. 앱을 실행해보자. 실행해보면 앱이 Launch 상태가 나오지 않는 것을 확인할 수있다.
didReceiveRemoteNotification이 호출되면서 네이티브에 FlutterViewController가 실행되기에 Flutter의 main 함수가 작동을 한다. 그래서 앱이 종료상태에 있었더라도 그냥 실행이 되버린다.
우리는 아이콘 뱃지 카운트만 올려주고 앱을 종료해 주어야 한다. 이러한 문제들 때문에 클라이언트에서 카운트 처리를 하는 것은 이해할 수 없는 작업이다. 서버에서 관리해야 한다.
어쨋든 클라이언트에서 처리를 해야하니 다른 방법을 시도해 보자.
저는 앱이 실행 중인 상태에서 카운트 처리를 하지 않고 있어서 active 상태는 제거할거고, inactive 상태에서도 푸시가 제대로 상태를 인식하지 못하기에 불안정한 요소를 제거하고자 해당 상태도 사용하지 않을 것이다. 오직 background 상태에서 처리할 것이다.
다시 코드를 수정하였다. 앱이 종료되었든 백그라운드 상태이든 네이티브에서 푸시 수신은 background 하나로만 수신받을 것이다. completionHandler이벤트를 호출하여 UIBackgroundFetchResult가 정상적으로 처리되었다는 알림을 주어야 하기에 코드를 추가한 것이다.
앱이 종료되었다면 아이콘 카운트 처리를 하고 앱을 다시 종료해야 하고, 앱이 백그라운드였다면 카운트 처리 후 종료를 하면 안된다. 이 부분은 아래에서 다시 살펴보자.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let state = UIApplication.shared.applicationState
if state == .background {
UIApplication.shared.applicationIconBadgeNumber += 1
}
completionHandler(UIBackgroundFetchResult.newData)
}
이번에는 앱의 생명주기인 LifeCycle에 대해서 살펴보자. Flutter의 라이프 사이클은 사용하지 않을 예정이고, swift에서만 라이프 사이클을 사용할 예정이기에 간단하게만 살펴보겠다.
위에서 살펴본 푸시 리스너에서 오직 background만 호출된다는 것을 확인했었는데, 우리가 필요한 부분은 진짜로 앱이 사용자에 의해 실행이된건지, 아니면 푸시 리스너에 의해서 실행이 된건지를 구분해야 한다. 그 작업을 하기 위해 라이프 사이클에 대한 상태 관리가 필요하다.
아래 코드는 앱이 백그라운드로 넘어가는 시점에 호출되는 리스너이다. 이 함수를 사용해서 구분할 예정이기에 우선 알아만 두면 된다. 자세한건 개발을 하면서 추가적으로 살펴보겠다.
override func applicationDidEnterBackground(_ application: UIApplication) {
//
}
이제 swift에서 푸시를 수신 받고 Badge카운트를 증가시키는 방법은 알았으니, Platform Channel을 통해서 flutter와 swfit 간의 채널을 연결시키는 작업을 진행해보자.
먼저 위에서 살펴본 swift 상태에서 푸시 수신시 푸시 호출 여부를 flutter에서도 전달 받도록 할 것이다. 이 작업이 필요한 이유는 밑에서 다시 설명할 예정이다.
Swift에 FlutterMethodChannel을 등록하자.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate ,MessagingDelegate {
var remoteListenerChannel : FlutterMethodChannel!
...
}
FlutterMethodChannel에 필요한 채널 이름과, messenger를 등록하도록 하자.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
remoteListenerChannel = FlutterMethodChannel(name: "tyger/remote/listener", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
위에서 푸시 수신시 호출되는 didReceiveRemoteNotification 리스너에 플랫폼 채널을 호출할 수 있도록 하자. 호출 콜 사인은 "background"로 호출할 것이고, arguments에는 우선 리턴 값은 없도록 하자.
헷갈리지 말아야 될 부분이 이건 swift -> flutter로 호출하는 거다. 이제 푸시가 수신이 되면 뱃지 카운트를 1증가 시키고, Method Channel을 통해서 Flutter에도 알림을 줄 것이다.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let state = UIApplication.shared.applicationState
if state == .background {
UIApplication.shared.applicationIconBadgeNumber += 1
remoteListenerChannel.invokeMethod("background",arguments: nil)
}
completionHandler(UIBackgroundFetchResult.newData)
}
Flutter에서도 MethodChannel을 등록하자.
final MethodChannel _remoterListenerChannel = MethodChannel("tyger/remote/listener");
setMethodCallHandler를 사용하여 콜 사인이 "background"였을 때만 처리할 수 있는 로직을 아래와 같이만 우선 해놓자.
void _remotePushListener() {
_remoterListenerChannel.setMethodCallHandler((call) async {
if (call.method == "background") {
//
}
});
}
이번에는 생명주기를 연결하는 Platform Channel을 등록할 예정이다. 등록 방법은 위에서 푸시 리스너를 연결한 것과 동일하다.
appLifeCycleChannel이라는 MethodChannel을 추가하자.
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate ,MessagingDelegate {
var appLifeCycleChannel : FlutterMethodChannel!
...
}
푸시 리스너 채널을 연결한 것과 동일하게 연결을 하는데, 채널 이름은 "tyger/lifeCycle"이렇게 등록하였다.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
appLifeCycleChannel = FlutterMethodChannel(name: "tyger/lifeCycle", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Life Cycle에 사용되는 기능인 백그라운드로 전환됬을 때 호출되는 부분에서 아래 MethodChannel을 실행시켜 flutter를 호출할 것이다. 여기서 콜 사인도 "background"로 지정하겠다.
override func applicationDidEnterBackground(_ application: UIApplication) {
appLifeCycleChannel.invokeMethod("background",arguments: nil)
}
이번에도 Flutter에 Method Channel을 등록하도록 하자.
등록 전에 상태 관리해야 할 변수가 하나있다. 편하신 방법으로 상태 관리를 진행해도 됩니다. 저는 싱글톤 패턴으로 관리하기 위해 싱글톤 모델을 하나 생성하였다.
isBackground라는 변수를 하나 등록하고 기본 값으로 false를 주었다.
class AppPlatformChannel {
static final AppPlatformChannel instance =
AppPlatformChannel._internal();
factory AppPlatformChannel() => instance;
AppPlatformChannel._internal();
...
bool _isBackground = false;
...
}
자 이제 상태 관리에 사용하는 곳에서 메소드 채널을 추가하자.
final MethodChannel _appLifeCycleChannel = MethodChannel("tyger/lifeCycle");
핸들러 부분에 작성된 코드이다. 콜 사인이 "background"로 들어오면 앱이 백그라운드 상태로 돌아갔다고 판단하고 위에서 선언한 isBackground 변수를 true로 변경해 주었다.
void _checkedAppLifeCycle() {
_appLifeCycleChannel.setMethodCallHandler((call) async {
if (call.method == "background") {
_isBackground = true;
}
});
}
이렇게 하게되면 Flutter에서 앱이 정상적인 실행인지, Remote Push 리스너에 의한 실행인지를 파악할 수 있다. 우리는 앱 실행 중 환경에서 앱 아이콘을 처리하지 않고 있으며, Remote Push 리스너에서 들어오는 상태는 background 상태 하나이기에, isBackground 값이 true일 때에만 앱을 종료하지 않아도 된다.
기존에 작성한 remotePushListener 함수에 분기 값을 추가하자. isBackground가 true면 앱 종료 이벤트를 실행해도 되는 것이다.
void _remotePushListener() {
_remoterListenerChannel.setMethodCallHandler((call) async {
if (call.method == "background") {
if (!_isBackground) {
// 앱 종료 이벤트 실행
}
}
});
}
이번에는 앱 종료 이벤트에 대해서 살펴보도록 하겠다. 앱을 종료하는 이벤트는 flutter에서 exit(0); 이것만 실행시켜주면 앱은 종료 된다.
하지만 저는 네이티브에서 처리하기로 하였다. flutter, 네이티브 중 어느 곳에서 처리해도 문제는 없을 것 같다.
이번에는 appForceExitChannel이라는 앱 종료 이벤트 MethodChannel을 등록해주자.
기존과는 다르게 flutter -> swift로 이벤트를 호출하는 것이기 때문에, setMethodCallHandler를 swift 코드에 넣어주면 된다.
콜 사인으로 "exit" 값이 들어오면 앱 종료 이벤트를 실행한다.
Flutter에서 IOS 앱 종료 이벤트에 실행하는 함수와 동일하다.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
let appForceExitChannel = FlutterMethodChannel(name: "tyger/exit",
binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
appForceExitChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
if(call.method == "exit") {
exit(0)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Flutter에도 앱 종료를 실행하라는 이벤트를 연결해 주자.
final MethodChannel _appForceExitChannel = MethodChannel("tyger/exit");
Method Channel을 실행하는 방법이다. 이제 해당 이벤트를 호출하면 swift가 앱을 종료할 것이다.
await _appForceExitChannel.invokeMethod("exit");
remotePushListener 함수를 다시 작성해보자. 이제 isBackground 변수가 true일 때, 앱 종료 이벤트 MethodChannel을 실행시켜 주면 앱 종료 상태에서 앱 뱃지 카운트만 1증가 시키고, 바로 앱은 종료 상태로 다시 넘어가는 것을 확인할 수 있다.
void _remotePushListener() {
_remoterListenerChannel.setMethodCallHandler((call) async {
if (call.method == "background") {
if (!_isBackground) {
await _appForceExitChannel.invokeMethod("exit");
}
}
});
}
Flutter에서 처리하고 싶다면 MethodChannel 연결하지 마시고 아래와 같이 하시면 된다.
void _remotePushListener() {
_remoterListenerChannel.setMethodCallHandler((call) async {
if (call.method == "background") {
if (!_isBackground) {
exit(0);
}
}
});
}
마지막으로 아이콘 뱃지를 초기화하는 방법에 대해서 살펴보겠다. 초기화는 간단하다. 위에서 앱 뱃지 카운트에 사용하던 기능을 0으로 변경해주면 초기화가 된다.
UIApplication.shared.applicationIconBadgeNumber = 0
초기화 작업에도 swift에서 초기화가 진행되어야 하기에 Platform Channel을 연결해 주도록 하자.
iconBadgeChannel이라는 MehtodChannel을 등록하도록 하자. 핸들러 부분에 콜 사인이 "reset"인 경우 0으로 만들어서 초기화를 해줄 수 있다.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
let iconBadgeChannel = FlutterMethodChannel(name: "tyger/icon/badge", binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
iconBadgeChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
if(call.method == "increment") {
UIApplication.shared.applicationIconBadgeNumber += 1
} else if(call.method == "reset"){
UIApplication.shared.applicationIconBadgeNumber = 0
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
뱃지 초기화를 위한 Method Channel을 등록해 보자.
final MethodChannel _iconBadgeChannel = MethodChannel("tyger/icon/badge");
초기화를 원하는 시점에 "reset"을 호출하여 MethodChannel을 호출하면 아이콘이 초기화 되는 것을 확인할 수 있다.
await _iconBadgeChannel.invokeMethod("reset");
각자가 원하는 시점에 초기화를 진행하면 되는데, 대부분 앱 시작시 또는 알림 페이지 진입시에 초기화를 진행한다.
IOS 앱 뱃지 작업하다가 뜬금없이 왜 안드로이드에 대해서 글을 작성하는지 의문이 들수 있지만 우리는 Platform Channel을 안드로이드에 등록하지 않았다.
그래서 지금 상태로 안드로이드로 앱을 실행하면 에러가 발생한다.
이걸 방지하기 위해 안드로이드에 실행되지 않는 MethodChannel을 등록해 주던가 아니면 Null 처리를 해주면 된다. 편안한 방법으로 처리하셔도 된다.
저는 Android에도 푸시 리스너 개발을 진행하고 있어서 kotlin 파일에도 MethodChannel을 등록해 두었다.
아래와 같이 등록을 해두어도 된다.
class MainActivity: FlutterFragmentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/remote/listener").invokeMethod("empty", null)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/lifeCycle").invokeMethod("empty", null)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/exit").setMethodCallHandler {
call, result -> if(call.method == "empty"){
result.success("success")
}}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "tyger/icon/badge").setMethodCallHandler {
call, result -> if(call.method=="empty"){
result.success("success")
}}
}
}
Platform 채널 등록을 하기 싫다면 Nullable로 처리를 해주면 된다.
final MethodChannel? _testChannel = Platform.isIOS ? MethodChannel("tyger/test") : null;
사용 전 Null처리만 해주면 된다.
if(_testChannel != null) {
await _testChannel.invokeMethod("null");
}
Flutter에서 서버 푸시를 수신 했을 때에 앱 아이콘 뱃지를 변경해주는 방법에 대해서 살펴보았다. 앱 아이콘 뱃지는 꼭 네이티브에서 처리하지 않아도 되고, flutter_app_Badger 라이브러리를 사용해서 Flutter에서 관리하셔도 된다.
네이티브에서는 현재의 뱃지 카운트 값을 가져올 수 있어서 좀 더 편하게 작업할 수가 있다.
이렇게 개발하면 앱 아이콘 뱃지를 처리할 수는 있지만, 클라이언트 측면에서 좋은 방식은 아닌 것같다. Apple에서도 정책상 앱 종료 상태에 대한 리스너를 제공하지 않은 부분이기 때문에, 서버에서 관리해서 카운트 값을 페이로드에 넣어서 푸시를 전송하는 방법이 베스트이다.
꼭 클라이언트에서 처리하고 싶다면 이런 방법도 있으니 참고하면 좋을 것 같다.
궁금하신 점은 댓글 남겨주시면 답변하도록 하겠습니다 !
안녕하세요, 뱃지 처리를 하려던 중 검색을 통해 오게 되었는데 좋은 정보 남겨주셔서 감사드립니다.
제가 개발 경험없이 flutter와 firebase로 앱을 만드는 중이라..설명을 보고도 긴가민가해서 질문 좀 드리겠습니다.
너무 초보적인 질문을 드린건 아닌지 민망하지만..그래도 질문드려 봅니다. ㅠㅠ
좋은 하루 보내세요. 감사합니다!