Upgrade to Pro — share decks privately, control downloads, hide ads and more …

2019 GDG Android - super.init() - ma'at pick for Flutter

Dora Lee
January 25, 2019

2019 GDG Android - super.init() - ma'at pick for Flutter

안녕하세요! 로플랫이란 실내위치 인식 기술을 보유한 회사에서 Android / iOS 개발자로 일하고 있는 이상훈이라고 합니다!

이번에 GDG Android에서 맛픽이란 서비스를 Flutter로 다시 개발한 개발기를 발표했습니다 :)

Dora Lee

January 25, 2019
Tweet

More Decks by Dora Lee

Other Decks in Technology

Transcript

  1. 안녕하세요! 저는 로플랫이란 실내위치 인식 기술을 보유한 스타트업에서 Android /

    iOS 개발자로 일하고있는 21살 개발자 이상훈입니다. “
  2. React Native vs Flutter 그 때 당시를 기준으로 라이브러리가 많다

    / 라이브러리가 많이 없다 레퍼런스가 많다 / 레퍼런스가 많이 없다 정식출시가 됐다 / 알파버전이다
  3. import 'dart:math' show Random; main() async { print('Compute π using

    the Monte Carlo method.'); await for (var estimate in computePi().take(500)) { print('π ≅ $estimate'); } } /// Generates a stream of increasingly accurate estimates of π. Stream<double> computePi({int batch = 100000}) async* { var total = 0; var count = 0; while (true) { var points = generateRandom().take(batch); var inside = points.where((p) => p.isInsideUnitCircle); total += batch; count += inside.length; var ratio = count / total; // Area of a circle is A = π⋅r², therefore π = A/r². // So, when given random points with x ∈ <0,1>, // y ∈ <0,1>, the ratio of those inside a unit circle // should approach π / 4. Therefore, the value of π // should be: yield ratio * 4; } } Iterable<Point> generateRandom([int seed]) sync* { final random = Random(seed); while (true) { yield Point(random.nextDouble(), random.nextDouble()); } } class Point { final double x, y; const Point(this.x, this.y); bool get isInsideUnitCircle => x * x + y * y <= 1; }
  4. import 'dart:math' show Random; main() async { print('Compute π using

    the Monte Carlo method.'); await for (var estimate in computePi().take(500)) { print('π ≅ $estimate'); } } /// Generates a stream of increasingly accurate estimates of π. Stream<double> computePi({int batch = 100000}) async* { var total = 0; var count = 0; while (true) { var points = generateRandom().take(batch); var inside = points.where((p) => p.isInsideUnitCircle); total += batch; count += inside.length; var ratio = count / total; // Area of a circle is A = π⋅r², therefore π = A/r². // So, when given random points with x ∈ <0,1>, // y ∈ <0,1>, the ratio of those inside a unit circle // should approach π / 4. Therefore, the value of π // should be: yield ratio * 4; } } Iterable<Point> generateRandom([int seed]) sync* { final random = Random(seed); while (true) { yield Point(random.nextDouble(), random.nextDouble()); } } class Point { final double x, y; const Point(this.x, this.y); bool get isInsideUnitCircle => x * x + y * y <= 1; } 이정도 문법이면, 할만한걸?
  5. @override Widget build(BuildContext context) { final bottomPadding = MediaQuery.of(context).padding.bottom; return

    new Scaffold( key: _scaffoldKey, backgroundColor: Colors.white, body: StoreConnector<ApplicationState, ForceTouchAction>( converter: (store) => store.state.forceTouchAction, builder: (context, action) { return new Stack( children: <Widget>[ Transform.scale( scale: 1.0 + (action.pressure == 0.0 ? 0 : action.pressure / 20), child: Stack(children: <Widget>[ NotificationListener<OverscrollIndicatorNotification>( child: PageView( controller: _pagerController, physics: NeverScrollableScrollPhysics(), children: <Widget>[MainMyMaatPage(), Container()], ), onNotification: (overscroll) { overscroll.disallowGlow(); }, ), Container( alignment: Alignment.bottomLeft, child: ClipRect( child: BackdropFilter( filter: ImageFilter.blur( sigmaX: Platform.isIOS ? 20 : 0, sigmaY: Platform.isIOS ? 20 : 0), child: Container( height: 64 + MediaQuery.of(context).padding.bottom, decoration: new BoxDecoration( color: Colors.black.withOpacity(0.2)), child: Container( child: Stack( alignment: Alignment.topLeft, children: <Widget>[ StoreConnector<ApplicationState, FoodReviewedLoadingState>( converter: (store) => store .state.foodReviewedLoadingState, builder: (context, loadingState) { if (loadingState != null && loadingState.isLoading) { return new SizedBox( child: LinearProgressIndicator( backgroundColor: Colors.transparent, valueColor: 방금 한말 취소.
  6. 맛픽 (ma’at pick) Flutter 개발기 Flutter React Native 처럼 리엑티브

    스타일 뷰 (Reactive Style View) 를 지원하는데, React Native의 JavaScript 브릿지 (브릿지)가 필요없고, Dart 라는 언어를 사용하여, 안드로이드 / 크롬 등에서 사용하는 Skia 엔진을 사 용합니다. Dart는 플랫폼에 따라서 네이티브 코드로 최적화되며, 퍼포먼스도 좋아집니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  7. 맛픽 (ma’at pick) Flutter 개발기 안드로이드에서는 Activity, iOS에서는 ViewController, 그럼

    Flutter 에서는? Flutter에서는 모두 ‘Widget’ 으로 통일합니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  8. 맛픽 (ma’at pick) Flutter 개발기 안드로이드에서는 Activity, iOS에서는 ViewController, 그럼

    Flutter 에서는? MaatPickSplashPageWidget ContainerWidget - BoxDecoration - LinearGradient ColumnWidget ImageWidget TextWidget LinearProgressBarWidget 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  9. 맛픽 (ma’at pick) Flutter 개발기 StatelessWidget과 StatefulWidget StatelessWidget은 Flutter가 처음

    위젯을 그리고 난 후에, 다시 그리지 않음을 의미합니다. StatefulWidget은 Flutter가 처음 위젯을 그리고 난 후에, 사용자의 반응, 특정 상태에 따라서 다시 그린다는 것을 의미합니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  10. 맛픽 (ma’at pick) Flutter 개발기 StatelessWidget과 StatefulWidget의 구현방식 class MaatPickApp

    extends StatelessWidget { @override Widget build(BuildContext context) { // 여기에 위젯 배치
 }
 } class GettingStartMaatPickButton extends StatefulWidget { @override State<StatefulWidget> createState() => new _GettingStartMaatPickButton(onPressed); } class _GettingStartMaatPickButton extends State<GettingStartMaatPickButton> { @override Widget build(BuildContext context) { // 여기에 위젯 배치
 } } 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  11. 맛픽 (ma’at pick) Flutter 개발기 StatefulWidget의
 setState() setState() setState() 01.

    UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  12. 맛픽 (ma’at pick) Flutter 개발기 @override Widget build(BuildContext context) {

    return Material( color: Colors.transparent, child: InkWell( onTap: this.onPressed, splashColor: const Color(0x40000000), highlightColor: const Color(0x20000000), child: Listener( onPointerDown: (status) { setState(() { _isTapped = true; }); }, onPointerUp: (status) { setState(() { _isTapped = false; }); }, behavior: HitTestBehavior.translucent, child: Container( height: 60, padding: EdgeInsets.only(top: 12, bottom: 12), decoration: BoxDecoration( border: Border.all( color: _isTapped ? Colors.orange : Colors.white), borderRadius: BorderRadius.circular(4)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Image.asset( 'assets/icons/google_g_logo.png', width: 34, ), Padding( padding: EdgeInsets.only(left: 24), child: Text( 'Join with google', style: TextStyle( fontFamily: 'Oswald', fontWeight: FontWeight.normal, fontSize: 22, color: Colors.white), )) ], ), ))), ); } setState(() { _isTapped = true; }); setState(() { _isTapped = false; }); _isTapped ? Colors.orange : Colors.white) StatefulWidget의
 setState() 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  13. 맛픽 (ma’at pick) Flutter 개발기 Hot Reload와 Hot Restart 코드를

    작성하고 Ctrl+S 를 하는순간 바로 변경사항이 반영됨 Hot Reload 현재까지 작성된 코드를 기반으로 앱 재시작 Hot Restart 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  14. 맛픽 (ma’at pick) Flutter 개발기 Material vs Cupertino Flutter에는 2가지의

    UI 타입이 있습니다. Material은 안드로이드 (혹은 웹 등)에서 사용하고 있는 UI 스타일이며, 흔히 말하면 “안드로이드 스타일” 입니다. Cupertino는 iOS UI 스타일이며, 흔히 말하면 “iOS 스타일” 입니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  15. 맛픽 (ma’at pick) Flutter 개발기 Material vs Cupertino 이미지 출처

    : https://pub.dartlang.org/packages/flutter_platform_widgets 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  16. 맛픽 (ma’at pick) Flutter 개발기 Material 사용 MaterialAppBarWidget CustomTabContainerWidget MaterialListViewWidget

    01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  17. 맛픽 (ma’at pick) Flutter 개발기 상태 관리 글로벌하게 상태를 저장하고,

    저장한걸 불러오고 해야할 때가 있습니다. 예를들어 API 호출을 한 후, 응답받은 다음 리스트를 보여줘야할 때 등이 있죠. 이럴 때 사용하는 것이 ‘상태관리’ 입니다. 상태관리에는 여러가지가 있지만, 제가 맛픽을 만들 때에 두가지 중 하나 (Redux / BLoC) 에서 고민했습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  18. 맛픽 (ma’at pick) Flutter 개발기 Redux 자바스크립트에서 앱에서 ‘상태(State)’ 를

    관리해주는 하나의 도구이며, Flutter에서도 비록 자바스크립트는 아니지만, 같은 역할을 합니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  19. 맛픽 (ma’at pick) Flutter 개발기 Redux Redux 에는 3가지의 원칙이

    있습니다. 1. 앱에는 ‘단 하나의 Store’ 만 사용함 2. Store 안에 있는 상태값은 모두 읽기전용 (Read-Only) 임 3. Store 안에 State 를 바꾸려면 action > dispatcher 를 사용해야하고, Reducer만 State 를 변경할 수 있음 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  20. 맛픽 (ma’at pick) Flutter 개발기 BLoC (Business Logic Components) 구글에서

    발표한 Dart 앱 비즈니스 로직 패턴입니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  21. 맛픽 (ma’at pick) Flutter 개발기 BLoC (Business Logic Components) MVVM

    패턴에서 ViewModel 이 BLoC 로 대체될 수 있습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  22. 맛픽 (ma’at pick) Flutter 개발기 BLoC (Business Logic Components) 01.

    UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter ੉޷૑ 출처 : https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
  23. 맛픽 (ma’at pick) Flutter 개발기 Redux 사용 매우 주관적인 의견이지만,

    ‘상태값’ 을 한 곳에 저장해서 쓰고, Action > Dispatch 관계로 호출하고, API는 미들웨어 (Middleware)에서만 호출하도록 하고, 상태값에 따라 위젯을 다르게 렌더링하는게 좋다고 판단하여 Redux 를 사용하기로 했습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  24. 맛픽 (ma’at pick) Flutter 개발기 Redux의 구조 ੉޷૑ 출처 :

    https://blog.novoda.com/introduction-to-redux-in-flutter/ 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  25. 맛픽 (ma’at pick) Flutter 개발기 Redux의 구조 사용자가 닉네임을 입력함

    (Action) 닉네임 중복검사 API 호출 (Middleware) 닉네임 중복체크 결과 Store에 저장 (Reducer) 닉네임 중복체크 상태 (Store) 닉네임 중복체크 상태 업데이트 됐으니 View 업데이트 (View) 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  26. 맛픽 (ma’at pick) Flutter 개발기 사용자가 닉네임을 입력함 (Action) 닉네임

    중복검사 API 호출 (Middleware) 닉네임 중복체크 결과 Store에 저장 (Reducer) 닉네임 중복체크 상태 (Store) 닉네임 중복체크 상태 업데이트 됐으니 View 업데이트 (View) Redux의 구조 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  27. 맛픽 (ma’at pick) Flutter 개발기 flutter_redux : https://pub.dartlang.org/packages/flutter_redux new StoreConnector<ApplicationState,

    List<FoodResponseResult>>( converter: (store) => apiResult.result, builder: (context, list) { return new ListView.builder( itemCount: list.length, itemBuilder: (context, position) { return _listItem(list[position]); }, ); }, ) 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  28. 맛픽 (ma’at pick) Flutter 개발기 코드 구조 라고 거창하게 써놓긴

    했지만.. 1. 아까 말씀드렸다시피 Flutter 에서 UI를 그리는 부분이 많이 가독성이 떨어 지기 때문에 이를 나름 해결했던 방법과 2. API 요청할 때에 API 모델을 만들어서 어떻게 보냈는지 를 다룹니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  29. 맛픽 (ma’at pick) Flutter 개발기 Widget build() 01. UI Type

    02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  30. 맛픽 (ma’at pick) Flutter 개발기 Widget build() @override Widget build(BuildContext

    context) { return Material( color: Colors.transparent, child: InkWell( onTap: this.onPressed, splashColor: const Color(0x40000000), highlightColor: const Color(0x20000000), child: Listener( onPointerDown: (status) { setState(() { _isTapped = true; }); }, onPointerUp: (status) { setState(() { _isTapped = false; }); }, behavior: HitTestBehavior.translucent, child: Container( height: 60, padding: EdgeInsets.only(top: 12, bottom: 12), decoration: BoxDecoration( border: Border.all( color: _isTapped ? Colors.orange : Colors.white), borderRadius: BorderRadius.circular(4)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Image.asset( 'assets/icons/google_g_logo.png', width: 34, ), Padding( padding: EdgeInsets.only(left: 24), child: Text( 'Join with google', style: TextStyle( fontFamily: 'Oswald', fontWeight: FontWeight.normal, fontSize: 22, color: Colors.white), )) ], ), ))), ); } InkWell PointerListener Container Row Image Padding Text 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  31. 맛픽 (ma’at pick) Flutter 개발기 다시 한번 보시죠 @override Widget

    build(BuildContext context) { return Material( color: Colors.transparent, child: InkWell( onTap: this.onPressed, splashColor: const Color(0x40000000), highlightColor: const Color(0x20000000), child: Listener( onPointerDown: (status) { setState(() { _isTapped = true; }); }, onPointerUp: (status) { setState(() { _isTapped = false; }); }, behavior: HitTestBehavior.translucent, child: Container( height: 60, padding: EdgeInsets.only(top: 12, bottom: 12), decoration: BoxDecoration( border: Border.all( color: _isTapped ? Colors.orange : Colors.white), borderRadius: BorderRadius.circular(4)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Image.asset( 'assets/icons/google_g_logo.png', width: 34, ), Padding( padding: EdgeInsets.only(left: 24), child: Text( 'Join with google', style: TextStyle( fontFamily: 'Oswald', fontWeight: FontWeight.normal, fontSize: 22, color: Colors.white), )) ], ), ))), ); } 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 끔-찍 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  32. 맛픽 (ma’at pick) Flutter 개발기 위젯을 그릴 때 컴포넌트 단위로

    끊자! @child Widget _closeButton() { return new Container( // 이하 생략
 ); } @child Widget _inputField() { return new Container( // 이하 생략 ); } @child Widget _nextButton() { return new Container( // 이하 생략 ); } @override Widget build(BuildContext context) { return new Scaffold( body: Stack( children: <Widget>[ _closeButton(), _inputField(), _nextButton() ], ), ); } 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  33. 맛픽 (ma’at pick) Flutter 개발기 API 모델 { “nickname”: “sanghun.lee”,

    “is_duplicate”: false } 아래와 같이 JSON 형태로 API 서버에서 응답이 온다고 해봅시다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  34. 맛픽 (ma’at pick) Flutter 개발기 API 모델 import 'package:json_annotation/json_annotation.dart'; part

    'nickname-duplicate-check-response.g.dart'; @JsonSerializable() class NicknameDuplicateCheckResponse { final String nickname; final bool is_duplicate; NicknameDuplicateCheckResponse({ this.nickname, this.is_duplicate }); factory NicknameDuplicateCheckResponse.fromJson(Map<String, dynamic> json) => _$NicknameDuplicateCheckResponseFromJson(json); Map<String, dynamic> toJSON() => _$NicknameDuplicateCheckResponse(this); } 아래와 같이 JSON 형태로 API 서버에서 응답이 온다고 해봅시다. 자동 생성 파일 해당 클래스는 JSON Serializable 하도록 함 서버에서 온 JSON을 객체로 만들어줌 (자동 생성된 코드 사용) 해당 객체를 JSON 형태로 변환해줌 (자동 생성된 코드 사용) 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter { “nickname”: “sanghun.lee”, “is_duplicate”: false }
  35. 맛픽 (ma’at pick) Flutter 개발기 API 모델 해당 코드를 flutter

    빌드 명령어를 통해 빌드하면, 아래와 같은 파일이 자동 생성됩니다. // GENERATED CODE - DO NOT MODIFY BY HAND part of 'nickname-duplicate-check-response.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** NicknameDuplicateCheckResponse _$NicknameDuplicateCheckResponseFromJson( Map<String, dynamic> json) { return NicknameDuplicateCheckResponse( nickname: json['nickname'] as String, is_duplicate: json['is_duplicate'] as bool); } Map<String, dynamic> _$NicknameDuplicateCheckResponseToJson( NicknameDuplicateCheckResponse instance) => <String, dynamic>{ 'nickname': instance.nickname, 'is_duplicate': instance.is_duplicate }; 원래 파일과 직접적인 링크 서버에서 온 JSON을 객체로 만들어줌 해당 객체를 JSON 형태로 변환해줌 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  36. 맛픽 (ma’at pick) Flutter 개발기 Native Component Flutter 에서 지원하지

    않은 네이티브 뷰 (비디오 플레이어 등), 네이티브 기능 (환경설정에 저장, 권한 요청 등)이 필요한 경우가 있습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  37. 맛픽 (ma’at pick) Flutter 개발기 Native Component Flutter 에서는 네이티브

    코드와 통신을 위해 MethodChannel API 와, PlatformView API, FlutterTexture API 등을 지원하고 있습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  38. 맛픽 (ma’at pick) Flutter 개발기 Native Component 네이티브 단에서 안드로이드에서는

    ExoPlayer 를, iOS 에서는 AVPlayer 를 사용합니다. VideoPlayer 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  39. 맛픽 (ma’at pick) Flutter 개발기 Native Component VideoPlayer Android iOS

    FlutterSurface FlutterTexture ExoPlayer AVPlayer Flutter TextureWidget 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  40. 맛픽 (ma’at pick) Flutter 개발기 Native Component VideoPlayer Android iOS

    .invokeMethod(‘play’) .play() MethodChannel ExoPlayer.play() AVPlayer.play() 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  41. 맛픽 (ma’at pick) Flutter 개발기 Native Component Camera 카메라도 마찬가지로,

    앞서 말씀드린대로 구현됐습니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  42. 맛픽 (ma’at pick) Flutter 개발기 Native Component 맛픽에서 사용된 /

    만든 네이티브 컴포넌트 비디오 플레이어 카메라 뷰 이미지 캐시 / 동영상 캐시 SharedPreferences (shared_preferences 라이브러리) 구글 지도 (google_maps_flutter 라이브러리) FCM (firebase_messaging 라이브러리) 권한 요청 (permission_handler 라이브러리) 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  43. 맛픽 (ma’at pick) Flutter 개발기 Native Component 라이브러리에 따라 안드로이드는

    Java / Kotlin 으로, iOS는 Objective-C / Swift 로 구현되어있음 자체구현한 네이티브 컴포넌트는 Kotlin / Swift 로 작성. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  44. 맛픽 (ma’at pick) Flutter 개발기 Performance Flutter에서는 디버그 빌드와 릴리즈

    빌드가 많이 다릅니다. 01. UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter
  45. 맛픽 (ma’at pick) Flutter 개발기 릴리즈 빌드 디버그 빌드 01.

    UI Type 02. State 03. Code Structure 04. Native Component 05. Performance 00. Flutter Android : 38.1MB iOS : 64.6MB Android : 11.7MB iOS : 22.3MB Hot Reload / Hot Restart 하기 위한 웹소켓 생성 코드도 있고… 디버깅을 위한 여러가지 코드 포함 앱 실행하는데 초점, 디버그에 필요한 코드 없음
  46. 맛픽 굿즈를 가져왔습니다. “ 스티커랑 병따개 챙겨왔어욤! 이따 스태프 분들께서

    주실거에요! 꼭 가져가주시고 잘 사용 부탁드립니다. 감사합니다 “