본문 바로가기

개발 코딩 정보 공유/개발후기

Flutter와 iOS 그리고 extension 간에 데이터를 공유 후기

 

안녕하세요. 블루스웨터소프트 입니다.

Flutter를 사용해보니 편리함과 동시에 앱의 복잡도가 증가했습니다.

개발 툴사용만 봐도 그렇습니다. 저는 vscode를 이용해서 flutter 코드를 작성합니다.

안드로이드는 android studio, ios 는 xcode 에서 따로 작업합니다. 

때문에 리소스도 상당히 많이 잡아먹는 작업입니다. 

그럼에도 Flutter의 많은 장점들 때문에 최근 가장 트랜디한 개발 언어가 되었겠죠.

 

개발을 하며 필수적으로 native 소스단에 접근하게 되는데요. 이럴때마다 선택지가 많다보니 어디에 핵심 로직을 둘것인가?하는 문제가 생기게 됩니다. 오늘은 개발시 있었던 일화와 간단한 값을 (읽기, 쓰기) 테스트 해보면서 좀더 이야기 해보겠습니다.

우선 제 상황을 공유하자면... 

  1. Flutter
  2. ios
  3. extension 

이렇게 크게 3가지의 파트로 나뉘어 집니다. sms 문자를 컨트롤 해야하는 상황이라 extension을 필수로 사용하게 되었습니다.

그러면서 자연스럽게 3 파트의 데이터 공유에 대해서 생각해보게 되었습니다. 데이터를 어디서 저장하고 어디서 활용해야 하지?

또 어떤 방식으로 저장을 할까? (shared preference, UserDefaults, realm 등)

 

Flutter의 Shared Preference 플러그인의 사용

해당 플러그인을 사용하면 android의 shared preferences 와 ios의 userDefault 와 유사하게 활용할수 있습니다.

간단한 데이터(iOS 및 macOS의 NSUserDefaults, Android의 SharedPreferences 등)를 위한 플랫폼별 영구 저장소를 래핑합니다

Supported data types are int, double, bool, String and List<String>.

 

1.  pubspec.yaml 파일에 shared_preferences 종속성으로 추가하십시오.

shared_preferences: ^2.2.2

 

2. 쓰기

final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString("userId", "kakao12345");
await prefs.setString("userLoginType", "kakao");

 

정말 간단하죠? 읽기도 확인해보겠습니다. 

 

3. 읽기

prefs.getString("userId")

 

읽기도 마찬가지로 너무 쉽습니다. (당연)  

쓰기(set)에서 await를 통한 비동기처리를 해주어야 합니다. 쓰기(set)시 비동기 처리가 필요한 이유는 쓰기 작업시 dart의 캐시를 활용하기 때문이라고 합니다. 때문에 가시성의 문제가 생길수 있습니다. (SharedPreferences.getInstance() = 싱글턴) 최신의 데이터를 보장해야 한다면 reload()를 통해서 캐시를 갱신해야 합니다. 

 

이렇게 쓴 데이터를 제 경우 ios 앱에서 활용했습니다. 과연 ios에서 잘 확인이 될까요?

네 당연히 됩니다. 대신 prefix 값을 붙혀주셔야 합니다.

UserDefaults.standard.object(forKey:"flutter.userId"))

 

이런식으로 말입니다. "flutter." prefix가 붙는 이유에 대해서도 문서 잘 설명되어 있습니다.

By default, the SharedPreferences plugin will only read (and write) preferences that begin with the prefix 
flutter.. This is all handled internally by the plugin and does not require manually adding this prefix.

 

그럼 이값을 extension 까지 끌고 갈수 있을까요? ios과 extension에서 데이터 공유를 위해 특별한 방법을 사용해야 합니다. 

바로 AppGroup을 등록해야 합니다.

 

capabillities의 App Group을 등록하세요.

 

앱 그룹을 생성하고 (apple dev) 필요한 extension도 생성합니다. 

 

데이터 공유를 위해서는 각각의 타겟별을 같은 AppGroup에 속하게 등록해줘야 합니다.

그리고 나서 데이터를 사용해보겠습니다. 

extension UserDefaults {
    static var shared: UserDefaults {
        let appGroupId = "group.ZNY6123123.com.blueswt.abc.smsfilter"
        return UserDefaults(suiteName: appGroupId)!
    }
}

 

편의성을 위해 UserDefaults 확장함수를 만들고 앱 그룹속성을 지정해줍니다. appGroupId는 당연히 등록할때 지정한 id 입니다.

사용방법은 동일합니다. 유틸클래스를 만들었다면 타겟 멤버쉽에 체크하는것을 잊으면 안됩니다

매번 깜빡하게되는 것

UserDefaults.shared.set("hello world", forKey: "intro")
UserDefaults.shared.synchronize()

 

해당 문장을 set 하고 extension에서 사용합니다.

let intro: String? = UserDefaults.shared.string(forKey: "intro")
print("=== \(intro) ===")

 

너무나 간단하게 값을 공유하게 되었습니다.

 

SMS Filter Extension 의 특수성

그런데 저의 경우 복병이 있었습니다. 제가 사용했던 MessageFilterExtension의 경우 아무리 이렇게 저렇게 해봐도 데이터 공유가 안되는 문제가 있었습니다. 너무 황당해서 realm도 써보고 모든 시도를 한 뒤에 문서를 다시 보기 시작했습니다. 

For privacy reasons, the system handles all communication with your associated server; your Message Filter app extension can’t access the network directly. Your app extension also can’t write data to containers shared with the containing app.

 

아 안된다고 하네요... 😅애초에 되는게 이상했던것입니다. 즉 ios 에서 쓴 데이터를 읽기만 가능하고 extension 에서는 쓰기 할수 없다는 말이었습니다. (보안상 문제)  혹시라도 해당 extension 을 사용하시는 분들에게 참고가 되면 좋겠습니다.

SMS and MMS Message Filtering = 데이터 공유 불가

꼭 확인 하시고 몇날 몇일 날리지 마시길 바랍니다🥲

그래서 이런 저런 방법을 생각하다가 flutter channel을 통해서 데이터(userId) 를 전달하고 ios에서 받아서 shared 처리한후 extension에서 읽기 하는 방법으로 최종 결정 내리게 되었습니다. 어떤게 정답이라고 할수는 없지만 제 경우 소스코드의 일관성이 중요했습니다. 어떤값은 flutter에서 처리하고 어떤값은 ios에서 처리하고 유지보수 측면에서도 꼭 필요한 기준이었습니다. (현장에서는 지켜지지 않지만🤪)

 

기타 lib 활용

이어서 ios 와 extension 사이의 realm 사용방법에 대해서 알아보겠습니다.

realm 이 atras device sdks 로 바뀐 모양입니다. 설치 방법 확인후 spm, cocoapod 어떤 방법이든 상관없이 편한 방식으로 설치를 해줍니다. https://www.mongodb.com/docs/realm/sdk/swift/install/

 

마찬가지로 appGroup의 id를 사용하여 realm을 인스턴스를 사용하게 됩니다.

var config = Realm.Configuration()
config.fileURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "appGroup.com.bluswt.abc.myapp")!.appendingPathComponent("default.realm")
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm()
class PatternData: Object {
    @Persisted(PrimaryKey: true)
    var number: Int // primary key로 지정
    @Persisted
    var blackWord: String
    @Persisted
    var date: Date

    convenience init(number: Int) {
        self.init()
        self.number = number
    }
}
func getPatternDataAll() -> Results<PatternData>{
  let objects = realm.objects(PatternData.self)
  return objects
}
 func addPatternData(data: PatternData){
        try! realm.write {
            realm.add(data)
        }
    }

 

 그 뒤에 읽고 쓰고 데이터를 자유롭게 공유 할수 있습니다.

 

마무리

앱 개발을 하며 생각하게 되는 데이터 저장 방식에 대해서 이야기 하였습니다. 일반적인 상황이 아니기 때문에 Flutter, ios, (android) extension 등 파트 별로 어떻게 데이터를 전달할지 굉장히 난해한 경우가 생깁니다. 기준을 잘 잡고 데이터가 어디서 어떻게 흘러가는지 잘 파악하고 구조를 잡는게 중요할것 같습니다.

 

*** sms filter extension 에서는 절대 데이터 저장을 하지마세요. 😭

 

참고문서

 

https://www.mongodb.com/docs/realm/sdk/?_ga=2.205025232.1589213261.1706596786-1205906347.1705670983

https://developer.apple.com/documentation/sms_and_call_reporting/sms_and_mms_message_filtering/

https://stackoverflow.com/questions/57608338/access-flutter-sharedpreferences-in-swift

https://pub.dev/packages/shared_preferences