본문 바로가기

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

Flutter 를 통한 앱개발 후기 이게 맞아?...

Flutter 를 사용하여 앱 개발 한다는건...

*플루터 플러터 등의 한글 표기가 상이하여 이하 영문 Flutter 로 통일하겠습니다.

 

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

오늘은 앱 개발시장의 화두인 Flutter 를 이용한 앱개발에 대해서 알아보겠습니다.

실제 프로젝트를 진행하며 느낀점과 정말 flutter를 이용한 앱개발이 쉬운것인지? 

그저 트랜디한 껍데기에 불과한지? 이야기 해보겠습니다.

 

최근 시장에서 Flutter 로 개발한 앱들이 심심치 않게 보이고 있습니다.

기업에서도 앞다투어 트랜디한 방식의 개발을 쫒고 있습니다.

Flutter는 크로스 플랫폼 개발 프레임워크 입니다.

한마디로 이야기 하자면 Flutter를 사용한 단한번의 코딩으로 ios, android, web, macox, window 등의 

개발을 쉽고 편한게? 진행할수 있습니다. 말만 듣고 보면 안쓸 이유가 전혀 없습니다.

 

장단점

장점이라고 한다면 원소스 코딩으로 멀티 플랫폼으로의 진출이 쉽다는것 입니다.

또한 네이티브 개발에 버금가는 속도를 보장한다고 하죠.

또한 구글의 오픈소스 skia 라이브러리를 사용해서 UI 를 렌더링 하기 때문에

ios 든 android 이든 일관된 UI 를 보장해 줍니다.

 

단점을 논해보자면...

일단 생각보다 빠르지 않다는 것입니다.

각종 플러그인 라이브러리 등이 붙고 하다 보니 빌드시 상당히 무거웠습니다.

 

멀티플랫폼을 지원하므로 기본소스가 상당히 무겁다.
ios, android 등의 네이티브 소스까지 따로 작업하게 될 경우 소스 용량이 상당히 증가한다.

 

현재 Flutter 를 위한 플러그인들이 많이 나와있고 특별한 기능이 아니고서야 네이티브 작업이 필요없을거라 생각했습니다. (착각)

하지만 우리는 신기하게도 매번 개발할때마다 특별한 기능을 만들고 싶어 하죠. (자의든 타의든...)

그래서 어짜피 개발시에 네이티브 소스를 건들여야 하는 불상사가 생기게 됩니다.

그렇다는 이야기는 곧 Flutter 개발자 가능 = IOS 개발가능 + Android 개발가능 이라는 공식이 성립됩니다. 🤪

또한 Flutter는 dart 라는 다소 생소한 언어를 사용하는데... 2011 년 출시하여 빛을 보지 못하다가 

Flutter 와 함께 주목을 ? 받기 시작했습니다.(끼워팔기?) 당연히 Flutter를 사용하려면 dart를 배워야 하겠죠.

dart 라는 언어는 swiftUI 등과 매우 유사합니다. 바로 선언형 프로그래밍 언어 이기 때문입니다.

실제로 사용해보면 얼마안가서 편리함과 동시에 미칠듯한 중첩 괄호들로 인해 머리가 지끈거리게 됩니다.

flutter 소스는 vs 에서 구성한다.

 

 

네이티브와 인터페이스

제가 개발한 앱에서 ios 의 extension 기능을 사용하기 위해서 네이티브 코드가 필수적이었습니다.

때문에 method channel을 활성화 하고 flutter와 ios, android 의 통로를 개방해주었습니다.

 

 

생각한 대로 라면 대략 이런 구조가 만들어지게 됩니다. UI는 Flutter를 통해 일관되게 구현하고 MethodChannel을 통해 네이티브 소스와 양방향 인터페이스가 가능한 구조 입니다. 이렇게 되면 기존 네이티브 진영에 존재하는 수 많은 서드파티 라이브러리를 가져다 쓸수도 있겠습니다. 논외로 native 기반 구조에 flutter 의 공통 화면을 추가할수도 있겠습니다.

 

MethodChannel의 사용은 매우 간단합니다.

Flutter에서 플랫폼별 채널을 설정하고

class ConstMember {
	static const ioPlatform = MethodChannel('ioPlatform/channel123');
}
 try {
      final String result =
          await ConstMember.ioPlatform.invokeMethod('saveUserData', {"userId": prefs.getString("userId")});
 ...
 }

 

ios 에서 아래와 같이 설정하고

let bridgeChannel = FlutterMethodChannel(name: "iosPlatform/channel123", binaryMessenger: controller.binaryMessenger)
bridgeChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
         	
      os_log("\(call.method)")

      switch call.method {
      case "saveUserData":
          guard let args = call.arguments as? [String : Any] else {return}
          let userId = args["userId"] as! String
          self?.saveUseSpecial(userId: userId)

          result("success") //flutter로 결과를 리턴한다.
          break
              
...

 

이런 식으로 Flutter 와 Native 환경 간에 인터페이스가 가능합니다. 안드로이드 또한 마찬가지 입니다.

어떤 기능은 flutter 에서 구현하고 어떤 기능은 native에서 구현할지 잘 판단해서 구현해야 합니다. 

앱이 커지면 굉장히 복잡도가 증가 할수 있을것 같습니다.

 

값 저장

간단한 값저장에 활용되는 SharedPreferences  UserDefaults 같은 경우 특별한 방식을 통해 저장값을 사용해야 합니다.

https://pub.dev/packages/shared_preferences 등의 플러그인을 활용하면 SharedPreferences 와 같은 기능을 Flutter 에서 

활용할수 있습니다. 

// Obtain shared preferences.
final SharedPreferences prefs = await SharedPreferences.getInstance();

// Save an integer value to 'counter' key.
await prefs.setInt('counter', 10);
// Save an boolean value to 'repeat' key.
await prefs.setBool('repeat', true);
// Save an double value to 'decimal' key.
await prefs.setDouble('decimal', 1.5);
// Save an String value to 'action' key.
await prefs.setString('action', 'Start');
// Save an list of strings to 'items' key.
await prefs.setStringList('items', <String>['Earth', 'Moon', 'Sun']);
// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = prefs.getInt('counter');
// Try reading data from the 'repeat' key. If it doesn't exist, returns null.
final bool? repeat = prefs.getBool('repeat');
// Try reading data from the 'decimal' key. If it doesn't exist, returns null.
final double? decimal = prefs.getDouble('decimal');
// Try reading data from the 'action' key. If it doesn't exist, returns null.
final String? action = prefs.getString('action');
// Try reading data from the 'items' key. If it doesn't exist, returns null.
final List<String>? items = prefs.getStringList('items');

 

//android
val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val userId = prefs.getString("flutter.userId", "") //flutter. prefix를 붙혀야 한다
//ios의 경우
let userDefaults = UserDefaults.standard
let userId = defaults?.string(forKey: "flutter.userId") //flutter. prefix를 붙혀준다

플랫폼별 저장위치가 상이하다

 

 

 

이밖에도 다양한 선택의 문제에 부딛히게 됩니다. 로그인을 예를 들어 보겠습니다. 로그인 기능을 구현하고 싶은데 화면은 Flutter 로 구현할지 네이티브를 따라 갈것인가? 화면만 flutter로 두고 기능은 네이티브로 구성할 것인가? flutter 로 전부 구현할 것인가?(가능하다면)

저 같은 경우 해당 기능을 구현하기 위한 plugin 이 지원된다면 우선 flutter로 최대한 구현하기로 하였습니다. 그 편이 유지보수 측면에서도 좋을거라 판단 했기 때문입니다.

 

모든것이 위젯

Flutter는 (dart 언어) 위젯이라 칭하는 오브젝트를 통해 UI를 구성합니다. Flutter의 특징을 말할때 꼭 나오는것이 바로 이 위젯의 역할 입니다. 모든것이 위젯이라는 말이 있을정도로 Flutter UI는 위젯들이 모여 하나의 화면을 구성하고 있습니다. 

기존의 명령형 프로그래밍에 익숙한 분들이라면 굉장히 난해할수 있는 구조 입니다.

위젯를 두고 위젯에 위젯에 위젯을 구현... 하여 하나의 화면을 만들게 됩니다.

@override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          backgroundColor: _genColor,
          shadowColor: Colors.white,
          title: Text(_title),
        ),
        backgroundColor: _genColor,
        body: ListView(
          children: [
            //top view
            Column(
              children: [
                SizedBox(
                  height: 20,
                ),
                Container(
                  color: _genColor,
                  height: 60,
                  alignment: Alignment.center,
                  child: Text(
                    '앱에서 스팸문자를 방어중 입니다.',
                    style: TextStyle(fontSize: 25, fontWeight: FontWeight.w500),
                  ),
                ),
                // Container(
                //   color: _genColor,
                //   height: 100,
                //   alignment: Alignment.center,
                //   child: Image.asset(
                //     'assets/images/img_home_sheld.png',
                //     width: 50,
                //     height: 50,
                //   ),
                // ),
                SizedBox(
                  height: 20,
                ),
              ],
            ),
            //recycle items
            getItemContaner(_patternImg, "유효패턴을 등록하고\n스팸을 방어하세요.", "패턴 설정", 0),
            getItemContaner(_aiImg, "ai를 통해 더 효과적으로\n방어할수 있습니다.", "철벽방어 이동", 1),
            //getItemContaner(_historyImg, "스팸처리 히스토리를 볼수 있어요.", "얼마나 많이 받고있을까요?", 2),
            getItemContaner(_settingImg, "일반 설정을 할수 있어요.", "설정하러가기", 3),
            SizedBox(
              height: 20,
            ),
          ],
        ));
  }

 

처음에는 어색했지만 익숙해지고 보니 이것처럼 간단한게 없을 정도로 편리했습니다. 또한 매우 직관적 입니다. 가로 세로 몇칸의 테이블 구조를 만들고 싶다? Column 과 Row 위젯을 활용하면 됩니다. 사이즈 여백을 두고 싶은데... SizedBox를 활용하거나 Spacer를 활용하면 됩니다. 뭘 넣어야 할지 모르겠는데 뭔가 큰 틀을 두고 싶은데... Container 를 사용하면 됩니다. 이렇게 위젯 하나하나의 타이틀이 굉장히 직관적이라 생각 하는대로 바로바로 표현이 가능했습니다.

 

Thread 관리

Flutter는 기본 구조가 싱글스레드로 동작합니다. async, await, future, isolate 등의 기법을 활용하여 멀티 스레딩 스러운 비동기 처리를 하게 됩니다. 앱에서 필수적으로 멀티스레딩 처리가 필요하다면 네이티브단에서 구현하는것이 좋겠습니다.

 

버전코드 관리

네이티브 버전코드를 아무리 고쳐도 변경점이 없길래 무슨 문제인지 한참 찾았던 적이 있습니다. Flutter의 pubspec.yaml 파일의 version 1.0.1+2 형태의 소스 수정을 통해 버전코드를 통일할수 있습니다.

version name = short : 1.0.1 
version code (build code) : 2

 

 

마치며

Flutter는 배우기 쉽고 편리합니다. 일관되게 UI를 통일할수 있으며 관리가 용이 합니다.

하지만 네이티브와 연결 하려고 한다면 복잡도가 상당히 증가하게 됩니다. flutter 소스, ios 소스, android 소스 필연적으로 기본 3벌을 관리해야 되는 불상사가? 생기게 됩니다. 배포 또한 플랫폼별 따로 따로 이기 때문에 굉장히 공수가 많이 들어간다고 보면 됩니다.

즉 flutter 를 써서 한방에 모든일이 해결될것 같지만... 3명의 일을 혼자할수도 있다. 정도로 마무리 하겠습니다. 😅

 

참고문서

https://aws.amazon.com/ko/what-is/flutter/

https://docs.flutter.dev/platform-integration/android/platform-views

https://docs.flutter.dev/platform-integration/platform-channels?tab=type-mappings-kotlin-tab