본문 바로가기
www/flutter

flutter 정리

by 금이아빠s 2023. 7. 27.

--

고민과 선택

현재 react로 웹, 모바일을 개발하고 있는 상황에서
repository를 합치는 작업을 진행하다 빠그러진 경험도 있고,
여기서 모바일을 개선한다고 했을때 native쪽도 무시할수 없는 상황.

이렇게 된거 모든걸 한쪽에서 고려하고, 배포의 번거로움과
시시때때로 변화대는 기획과, react -> native 전환 속도 이슈를 해소 하기위해
react native, flutter를 고민해 보았습니다.



[?] native vs (react native, flutter) 성능면에서 만족 시킬수 있을까?
   - 성능면에서는 이길수 없습니다.
      그래서 체감상 어느정도의 차이가 나는지 직접 느껴보고 싶어서 준비하고 있습니다.

[?] react native, flutter 중 flutter를 선택한 이유
    - 성능면에서 '빠른걸 선택하자'가 1번.
      native에서 사용되는 기술을 모두 사용할수 있는지 2번
      현재 서비스중인 기능이 다 되는지 (seo 지원) 3번

[?] react 코드를 react native로 변경하는 작업만 하면되는거아냐?
   - 사용하는 방식이 다르기에 비슷하지만 새로 다 만들어야 합니다.
      html, less(css)를 사용할수 없습니다.
      (flutter, RN 이나 접하기 위해서는 스터디 필요)
      * 기간은 2달정도 예상

[?] react native = aos, ios, web
[?] flutter = aos, ios, web(canvas기반)
[?] 레퍼런스 : react native > flutter 
     - output은 같지만 react native는 제이에스브리지(JSBridge) 가 중간에 있기때문에
     성능적으로 flutter가 더 좋다고 판단 했습니다.

[?] 배포
    - react native는 1회 심사후 수정된 코드를 서버에 배포. (최고의 장점)
    - flutter, native 매번 심사 후 배포.

[2023.09.20 update] shorebird 를 통해서 clode push가 가능하다!!!
이렇게 되면 react native화 같이 1회 심사후 수정된 코드를 바로 배포할수 있다!!!
요!!!!! google에서 어여 정식버전으로 지원이 되면 좋겠다.


[?] 어느부분때문에 native > flutter 성능차이가 나는걸까?
   - flutter는 JIT(Just-In-Time) 컴파일러를 사용
      native는 AOT(Ahead-Of-Time) 컴파일러를 사용
      이 컴파일러 자체 성능이 AOT가 더 좋다.
   - flutter가 레이아웃을 계산하는 부분에서 CPU를 더 많이 쓴다.
   - flutter는 2D 앱을 개발하는대 최적화가 되어 있어서 3D 개발은 성능이 많이 떨어진다.
      *레이아웃계산이 늦으니 당연한 말이겠지만..

* 성능비교
https://yozm.wishket.com/magazine/detail/567/

 

 

확실히  Native로 만든것만큼의 성능은 뒤떨어진다 생각합니다.
Native > flutter > react native
효율적인 면 = 시간 + 인력 + 비용 을 따져가며 고려를 하면 좋을것 같다는 생각이 들었습니다.


 

---

한달 동안 개발하고 느낀점.

 

거의 한달 정도를 작업해본것 같다.
그것도 다른일 하면서 틈틈히 한건대 해보니까 
좋은건 알겠는대 화면을 그리는 레이아웃을 익히는걸 알아야 할것같다.
html과 style처럼 layout을 다루는 부분을..

성능 적인 부분은 아직 멀 많이 넣지 않아서 일까?
react보다는 조금 성능이 좋아보인다.
그리는 방식도 canvas로 그리는 방식이고,
빠른 인터렉션을 넣으니 확실히 cpu는 올라가는게 보인다.

기술적인 부분은 생각보다 막 어렵진않았지만
class 함수를 이쁘게 작성하고, type을 지정해주는 부분 및 data값을 정의 해 주는 부분에서
처음에 좀 해매는 부분이 있었다.

service, provider, state, model 들을 정의하고 
가져와 사용하고 하는 부분은 어렵지 않았던것 같다.

 

---
flutter 정보.

현재 flutter --version
Flutter 3.10.6 • channel stable • https://github.com/flutter/flutter.git

Framework • revision f468f3366c (3 weeks ago) • 2023-07-12 15:19:05 -0700
Engine • revision cdbeda788a
Tools • Dart 3.0.6 • DevTools 2.23.1

렌더링 방식: Impeller
react에서는 버전이 올라갈 때 어떤 기능은 안 되는 게 있고 어떤 건 사라지고 호환성에 대한 걱정을 좀 해야 한다.
하지만 flutter에서는 이전에 사용하던 친구들을 모두 지원해주고 있다.
고로. 버전을 올리는 거에 대한 거부감은 덜할 것 같다.
라고 보았지만 테스트 코드를 작성하다보니 http쪽이였나.. 이전버전에서 사용하던 코드를 최신으로 올리니 안되었던것 같기도한대..

변화된 내용 : https://chaevid.com/30
package : https://pub.dev/packages
test코드 작성 : https://dartpad.dev

 

 


 

나와라 뚝딱!

 

 

 


아래로는 개발관련.


 

Lifecycle

[StateFullWidget]
- Constructor
- createState()

[State]
- initState()
- didChangeDependencies()

- build()
- didUpdateWidget()
- setState()

- deactivate()
- dispose()


----

[위젯 구축]
- Constructor
- createState()
- initState()
- didChangeDependencies()

[드로잉]
- build()
- didUpdateWidget()
- setState()

[위젯파기]
- deactivate()
- dispose()

 

 

---

폴더구조

assets : fonts, images
lib: 
 - screens | pages : 화면
 - widgets : 위젯 (component)
 - utils : function이나 logic
 - providers : 앱 외부의 다른 서비스들과 앱을 연결하는 코드
 - models : 데이터 모델
 - services : 데이터를 주고받는 요소
 - route.dart : 경로 정의
 - constants.dart : API 엔드포인트, 테마, 상수 정의

 

---

Naming Convention

- 라이브러리, 패키지, 디렉토리, 소스파일 등: lowercase_with_underscores
- 클래스명, 타입 : PascalCase
- 변수명, 함수명, 파라미터 : camelCase

 

 

 

 

 

 

 

 

 

 

 

---

설정파일

- pubspec.yaml 
flutter에 library를 추가한다거나 이미지를 넣는다거나 할 때, 추가해야 할 곳.

dependencies:
  # 이미지 첨부
  image_picker: ^1.0.1
  # http
  http: ^1.1.0
  # websocket
  web_socket_channel: ^2.4.0
  # 상태관리
  provider: ^6.0.5
  # router
  go_router: ^10.0.0
  # seo
  meta_seo: ^3.0.5
  # json
  json_annotation: ^4.8.1
  # 숫자 표기
  intl: ^0.18.1
  # 네트워크
  connectivity_plus: ^4.0.2
  
  
dev_dependencies:
  # json 직렬화 패키지
  json_serializable: ^6.7.1
  # json_annotation build
  build_runner: ^2.4.6

  
assets:
  # 이미경로
  - assets/img/test.png

 

- ios/Runner/Info.plist
ios에 권한 추가해야 할 때 이곳에 추가.

<key>NSContactsUsageDescription</key>
<string>NSContactsUsageDescription : 연락처 접근 권한</string>
<key>NSCameraUsageDescription</key>
<string>NSCameraUsageDescription : 카메라 접근 권한</string>
<key>NSMicrophoneUsageDescription</key>
<string>NSMicrophoneUsageDescription : 마이크 사용 권한</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>NSPhotoLibraryUsageDescription : 사진 라이브러리 권한</string>
<key>UILaunchStoryboardName</key>
<string>UILaunchStoryboardName : 앱이 처음 시작될 때 런치 스크린(로딩 화면)을 설정</string>

*key, string이 한쌍인것 같다.
  key만 입력시 err발생. key와 boolen과도 한쌍인듯.

 

- analysis_options.yaml
lint설정은 여기서 할수 있습니다.
기본적으로 설정은 되어 있음.

include: package:flutter_lints/flutter.yaml

 

---
class 객체지향프로그래밍(oop)

  • export : 자식class에 부모class를 상속.
  • implements : 클래스에 interface 연결.
  • with : 자식class에 부모class의 기능을 추가.
  • mixin: 자식class에 부모class의 기능을 일부 추가.
  • abstract : 클래스를 추상 클래스로 선언합니다.
  • final : 클래스를 변경할 수 없는 클래스로 선언합니다.
  • const : 클래스를 상수 클래스로 선언합니다.

 

---
model

데이터를 model로 정의할때 쉽게 요 아래에서 정의가 가능.
https://javiercbk.github.io/json_to_dart/


써보니 나는 그냥 좀 별로였다.
그 이유는 model을 정의하는대 모든값에 ?값이 붙는다.
null을 대비한 값이라니..
type을 정의하고 딱딱! 정의해서 쓰고 싶어서 쓰는 친구인대.. ?가 들어가는 순간 별로였다..

# 설정파일
/pubspec.yaml
- json : json_annotation + json_serializable + build_runner

# script
- library 세팅: flutter pub get

- model build:&nbsp;flutter pub run build_runner build

 

_jsonMap -> json 으로 데이터 타입을 변경하기 위해서 위에 처럼 library를 설치하고,
build까지 해주었다.

왼쪽코드를 작성하고 빌드하면 오른쪽 처럼 파일이 만들어진다.
데이터 형변환을 할려고 3일 걸린것 같다.. 휴..

 

검색하는것 마다. 'dart:convert' 를 import 해서 jsonDecode 해서 사용하라고만 나온다,,
그래서 그렇게 사용하면 model에 정의한 것과 다르다, _jsonMap 타입이라 Map<String, dynamic>로 변경할수 없다.
이런 에러만 주구장창.. 

 

 

---

큰 틀

MaterialApp (root에서 설정) -> Scaffold (page에서 설정)

MaterialApp은 앱 기능을 할 수 있도록 해주는 뼈대,
Scaffold은 앱 디자인적인 부분에서의 뼈대 정도로 생각하면 된다.


MaterialApp 

title: 애플리케이션의 제목입니다.
home: 애플리케이션의 홈 페이지입니다.
theme: 애플리케이션의 테마입니다.
routes: 애플리케이션의 라우팅 경로입니다.
onGenerateRoute: 라우팅 경로를 생성하는 함수입니다.
onUnknownRoute: 알 수 없는 라우팅 경로를 처리하는 함수입니다.
debugShowCheckedModeBanner: 디버그 모드 배너를 표시하는 여부입니다.
locale: 애플리케이션의 언어입니다.
supportedLocales: 애플리케이션이 지원하는 언어 목록입니다.
showPerformanceOverlay: 성능 오버레이를 표시하는 여부입니다.
showSemanticsDebugger: 시맨틱 디버거를 표시하는 여부입니다.
checkerboardRasterCacheImages: 래스터화된 이미지를 검사판 모드로 표시하는 여부입니다.
checkerboardOffscreenLayers: 오프스크린 레이어를 검사판 모드로 표시하는 여부입니다.
typography: 애플리케이션의 타이포그래피입니다.
visualDensity: 애플리케이션의 시각 밀도입니다.

 

Scaffold

appBar: 앱바입니다.
body: 앱바 아래의 영역입니다.
bottomNavigationBar: 바텀 네비게이션 바입니다.
floatingActionButton: 플로팅 액션 버튼입니다.
drawer: 서랍입니다.
persistentFooterButtons: 영구적인 푸터 버튼입니다.
backgroundColor: 배경색입니다.
elevation: 앱바, 바텀 네비게이션 바, 플로팅 액션 버튼의 높이입니다.
floatingActionButtonLocation: 플로팅 액션 버튼의 위치입니다.
drawerScrimColor: 서랍의 스크림 색입니다.
drawerEdgeDragWidth: 서랍을 드래그할 수 있는 너비입니다.
resizeToAvoidBottomInset: 바텀 네비게이션 바가 화면 하단에 겹치지 않도록 조정하는 여부입니다.
scaffoldMessenger: Scaffold 위젯에서 발생하는 메시지를 처리하는 객체입니다.

 

scaffold -> body

child: body 속성의 자식 위젯입니다.
decoration: body 속성의 배경 장식입니다.
padding: body 속성의 여백입니다.
constraints: body 속성의 제약 조건입니다.
margin: body 속성의 여백입니다.

 

Routes
*go_router 10.0.0

class Routes {
  Routes._();

  static GoRouter routers(isIntroRead) {
    return GoRouter(
      // 라우터의 네비게이터 키
      navigatorKey: _rootNavKey,
      // 라우터가 시작할 때 로드할 URL
      initialLocation: ERoutes.ROOT,
      //  라우트를 정의
      routes: [
        GoRoute(
            path: ERoutes.ROOT,
            builder: (context, state) {
              return Loading(isIntroRead, const MyHomePage());
            },
            routes: [
              GoRoute(
                path: ERoutes.HOME,
                builder: (context, state) => const MyHomePage(),
              ),
            ]),
        GoRoute(
          // name:
          path: ERoutes.PAGE1,
          builder: (context, state) => Page1(),
          // sub router
          // routes: []
        ),
        GoRoute(
          path: ERoutes.PAGE2,
          builder: (context, state) => const Page2(),
        ),
        GoRoute(
          path: ERoutes.PAGE3,
          builder: (context, state) => const Page3(),
        ),
      ],
      // 라우터에서 예외가 발생할 때 호출
      onException: (context, state, router) => const Page404(),

      // errorPageBuilder: 라우터에서 예외가 발생하고 onException 함수가 호출되지 않을 때 표시되는 페이지를 빌드하는 함수
      // errorBuilder : 라우터에서 예외가 발생하고 onException 함수가 호출되지 않을 때 표시되는 위젯을 빌드하는 함수
      // redirect : 특정 경로로 리디렉션
      // refreshListenable : 라우터가 새로 고쳐질 때 호출되는 리스너
      // redirectLimit : 라우터가 리디렉션할 수 있는 최대 횟수
      // routerNeglect : 라우터가 예외를 무시할지 여부
      // initialExtra : 라우터가 시작할 때 사용할 추가 데이터
      // observers :
      // debugLogDiagnostics : 라우터의 디버그 로그를 활성화
      // restorationScopeId : 라우터의 복원 범위 ID
    );
  }
}

go_router 를 사용해서 대메뉴, 서브 메뉴를 지정할수 있다.
다만 써보니까 좀 불편한 감이? 
root path = '/메인'
sub는 '서브' 이렇게 /를 빼야 한다.
그런대..

context.go('/서브');

페이지를 이동시킬때는 이렇게  /를 붙여야 한다....머냐..;;;
내가 모르는 먼가가 있겠지..

 

---

위젯

## layout

body: Column(
	children: <Widget>[
        Container(
       		child: ...
        ),
        Row(
        	children: ...
        ),
        Center(
        	child: ...
        )
    ]
)


## input
TextFormField(
	keyboardType: TextInputType.emailAddress, //input type
    decoration: InputDecoration(
      hintText: '이메일',
      border: OutlineInputBorder()
    ),
),

## 공백
 SizedBox(height: 15.0, width: 10)

 

 

---

변수, Type

[Type]

int : 정수
double : 실수
String : 문자열
bool : 참/거짓
List : 배열
Map : 맵
Set : 집합
Rune : 유니코드 문자
Object : 모든 객체의 최상위 클래스
dynamic : 타입이 지정되지 않은 값
---

[변수 선언]

final: 변수의 값을 한 번만 할당할 수 있는 경우 사용합니다.
static: 클래스 내에서 공유되는 변수나 함수를 선언할 때 사용합니다.
var: 변수의 타입을 지정하지 않고 선언할 때 사용합니다.
const: 컴파일 타임에 값이 결정되는 변수나 함수를 선언할 때 사용합니다.
Future: 비동기 작업의 결과를 나타내는 변수를 선언할 때 사용합니다.
void: 함수가 값을 반환하지 않는 경우 사용합니다.

- final String sOs1 = value;
- const String sOs2 = value;
변경되지 않는 값 정의
* final은 dateTime과 같은 빌드 time을 얼아야 할때 사용

- _변수명  private variable
* private variable은 파일 단위로 접근이 불가능한 변수

- List<String> array
List<String> arrData = ['a','b','c'];

- Map<String, String> object
Map<String, String> oData = {
	'a': 'A',
    'b': 'B'
}
// 데이터 추가
oData.addAll({
	'c': 'C'
});
// 데이터 변경
oData['a'] = 'AA';
// 데이터 삭제
oData.remove('a');
oData.keys.toList() // key값 => ['a','b','c']
oData.values.toList() // value => ['AA', 'B', 'C']

// model 정의
enum eData {
	a = 'A';
    b = 'B';
}

void fnTest(){
	eData data = eData.a;
    ....
}

 

 

---

class, interface, mixin

parameta, typedef, Operation

void main() {
  Aa aa = Aa('ssss test');
  
  print(aa.name);
  aa.setA('ss');
  print(aa.name);
  
  /// string[]
  List<String> arrValues = [
    'a', 'b', 'c', '1'
  ];
  
  // 처음거
  print(arrValues.first);
  // 비어있니?
  print(arrValues.isEmpty);
  // 안비어있지?
  print(arrValues.isNotEmpty);
  // 갯수
  print(arrValues.length);
  // 마지막꺼
  print(arrValues.last);
  // 정렬 반대로
  print(arrValues.reversed);
  // string 추가
  arrValues.add('2');
  // array 추가
  arrValues.addAll(['3', '4']);
  // a 삭제
  arrValues.remove('a');
  // 1번 삭제
  arrValues.removeAt(1);
  // roop돌면서 찾아서 삭제
  arrValues.removeWhere((value) => value == '1');
  
  
  /// {string: number}[]
  List<Map<String, dynamic>> arrOdata = [
    {'name': 1, 'title': '1'},
    {'name': 2, 'title': '2'}
  ];
  // key값
  print(arrOdata[0].keys);
  
  // object에 key가 있다면 값변경 없다면  {key: 22} 추가
  arrOdata[0].update('names', (prev){
    return prev * 100;
  }, ifAbsent: (){
    return 22;
  });
 
  // object에 값 추가
  arrOdata[0].putIfAbsent('age', () => 1111 );
  
  // object를 돌면서 추가
  arrOdata[0].updateAll((key, value){
    return value.toString() + '원';
  });
  
  // object 에서 key값이 같으면 삭제
  arrOdata[0].removeWhere((key, value){
    return key == 'name';
  });
  
  
  print('===========${arrOdata}');
  
  // item.name은 메서드일때, key값 들고 올때는 ['key']
  //print(arrOdata.firstWhere((item) => item['name'] == 1));
  // index
  print(arrOdata.indexWhere((item) => item['name'] == 1));
  // forEach
  void fnPrint (item){
    print(item);
  }
  arrOdata.forEach(fnPrint);
  // map
  print(arrOdata.map((item) => item['name']));
}

class Aa implements AaInterface{
  String name;

  Aa(this.name);

  @override
  void setA(String name) {
    name = name;
  }
  
  void fnB(){
    print('hi');
  }
  void fnC(){
    print(':)');
  }
}

class AaInterface{
  void setA(String name){}
}

*interface 정의는 implements로 연결해서 사용을 한다.
다만 이렇게 사용할 때의 단점? 장점? 정의가 안되었다고 바로 알 수 있다.
하지만 interface에 정의된 메서드 위에는 @override가 정의돼야 한다.

@override를 사용했을 때 
재정의를 하는 부분이라 느려지지 않을까?라는 생각이 들었다.
이건 추후 확인 좀 해봐야겠다.

 

---

get, set

 void main(){
	Aa aa = Aa('ssss test');
    
    print(aa.name);
}


class Aa{

	String name;
    
    Aa(this.name);
    
    get getA{
    	return name;
    }
    
    set setA(String name){
    	name = name;
    }
}

 

---
websocket

나도 모르게 소켓이 좋아진건지 익숙해진건지..

web_socket_channel 를 사용해서 소켓연결

하위 속성
- sink: 메시지를 보낼 수 있는 객체입니다.
- stream: 메시지를 받을 수 있는 객체입니다.
- close: 연결을 닫는 함수입니다.
- onDone: 연결이 완료되면 호출되는 함수입니다.
- onError: 연결이 오류가 발생하면 호출되는 함수입니다.

기존에 javascript로 사용하던 방식과는 조금 다르다.

 late WebSocketChannel _webSocket = WebSocketChannel.connect(Uri.parse(url));
 _webSocket.then((socket) {
 	  // 이벤트 send
      socket.sink.add(eventSink);
      socket.stream.listen((message) {
        // debugPrint(message);
      }, onDone: () {
        debugPrint('onDone');
      }, onError: (error) {
        debugPrint('[socket error] : $error');
      });
    });

 

연결하고 소켓을 받는 방식은 이렇다.

 

---
상태관리

기본적으로 제공되는 setState가 있지만
getx, Provider를 가장 많이 사용.

*getx가 의존성 주입이 불가능 하다고 하는대 확인을 좀더 해봐야 알겠지만
GetxInjector를 통해서 가능 하다고 하니 확인은 필요해 보임.

 

bard가 알려준 성능차이

 

getx가 많은 기능을 제공하고
실행을 하거나 변수값을 가져올때나, 라우터 이동을 할때도 쉽다고해서
써볼까 하다가.. 성능 적인 면에서 너무 떨어져서 provider로 결정..

void main() {
  runApp(MultiProvider(providers: [
    // intro
    ChangeNotifierProvider(create: (_) => IntroProvider()),
    // trade
    ChangeNotifierProvider(create: (_) => TradeDataProvider()),
    // example
    ChangeNotifierProvider(create: (_) => ExampleModel())
  ], child: const MyApp()));
}


// provider 접근 및 함수 실행
Provider.of<IntroProvider>(context, listen: false).getApiIntro();

[State -> build]
final introProvider = context.read<IntroProvider>();
final apiCall = context.select<IntroProvider, int>((procider) => procider.apiCall);

 

list.dart

class ListWidget extends StatelessWidget {
  const ListWidget({super.key});

  List<Card> _listData(BuildContext context) {
    final introProvider = context.watch<IntroProvider>();
    final coinsOnMarketList = introProvider.getCoinsOnMarketList();
    final tradeDataProvider = context.watch<TradeDataProvider>();
    final getTradeData = tradeDataProvider.getTradeData;

    // introData && tradeData
    if (coinsOnMarketList != null && getTradeData.keys.isNotEmpty) {
      for (final marketType in coinsOnMarketList.keys) {
        final coins = coinsOnMarketList[marketType];
        if (coins != null) {
          return List.generate(coins.length, (index) {
            final MTradeDataCoin coinTradeData = tradeDataProvider
                .getTradeDataCoin(marketType, coins[index]['coinType']);
            return Card(
                color: Colors.teal[100 * (index % 9)],
                child: coinRow(coins[index], coinTradeData));
          });
        }
      }
    }

    return List.generate(
      1,
      (index) => Card(
        color: Colors.teal[100 * (index % 9)],
        child: const NoList(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, provider, wideget) {
      return SliverList(
        delegate: SliverChildListDelegate(_listData(context)),
      );
    });
  }
}

위 코드에서 보면 build 에서 내려오는 context를 통해서 provider를 받아서 사용할수 있다.
이렇게 사용하려면  Consumer<PROVIDER_NAME>을 통해서 받아올 provider를 정의해줘야한다.

이렇게 하면 1개의 provider만을 사용할수 있기에..
*.. Consumer<provider1, provider2> 이게 될것같은대.. 왜안될까.. 삽질을좀해봤다.. 안되는건.. 안되더라

 

 final tradeDataProvider = context.watch<TradeDataProvider>();
 final getTradeData = tradeDataProvider.getTradeData;

이렇게 context안에서 provider를 골라서 사용할수 있다.
음음.. 이걸 엄청오래 걸려서 알았다.. 2일은 걸린듯... 휴..


 

---
빌드

flutter run
- 빌드할 디바이스 선택.

flutter run
ios 빌드된 모습

아직 코드 양이 적어서 그렇겠지만 빌드 속도는 빠름.

simulator를 vscode에서 열때는 
단축키 [option + shift + p] -> [Flutter: Select Device] -> [세팅되어있는 디바이스 선택(Pixel2m, iphone, chrome, macOS]

세팅되어있는 디바이스 목록

 

---
버전업

flutter upgrade

 

작업을 하다보니 http, websocket들이 버전이 많이 올라가서 최신버전으로 올려서 빌드를 해보니
두둥! error...

library도 최신, flutter도 최신! :) 굿굿
(신상이 최고)

호환이 되는 버전보다 낮아서 안된다고..