Search

OTL Flutter 앱 위젯이 릴리즈에서 작동하지 않았던 이유 (Stack > Expanded)

Tags
SPARCS
OTL
개발
Flutter
수정일
2024/05/29 11:46
작성일
2024/05/28

정재현(edge)님의 OTL 팀 참여

안녕하세요, SPARCS OTL 팀 Tech Lead 오승빈입니다.
저희 KAIST 학부 총학생회 산하 SPARCS는 인턴 제도를 운영하고 있는데요. 매 학기 단체에 새로 들어온 신입 회원들은 한학기동안 신입생 세미나와 신입생 프로그램이라는 공통 온보딩 교육을 듣게 됩니다. 하지만 이 제도의 경우 이미 개발을 어느 정도 경험한 회원들은 이미 알고 있는 내용으로 구성된 세미나를 다시 들어야 하고, 신입생 프로젝트가 발표 및 심사용으로만 개발되고 버려진다는 문제점이 있습니다. 그래서 제가 회장으로 취임한 2022년 봄학기부터 인턴 제도를 신설하게 되었습니다! 인턴으로 선발된 팀원들은 첫 학기동안 프로젝트 팀에 소속되서 실제 배포되는 기능에 기여하고, 해당 내용으로 발표를 진행하여 준회원에서 정회원으로 승급합니다.
이번 2024년 봄학기 OTL 팀에서는 두명의 인턴 분께서 참여해 주셨습니다. 앱 파트 정재현(edge)님과 디자인 파트 김희진(gimme)님이신데요. 오늘은 정재현 님과 OTL 앱 졸업플래너 탭 기능추가를 하며 겪은 이야기를 해볼까 합니다.
2024년 봄학기 OTL 팀 구성 @조유민

OTL팀 앱 인턴 과제

이번 앱 파트 인턴 과제는 아래의 순서로 구성하였습니다.
1.
Github Issue, Pull Request 등 팀 협업 방식 온보딩
2.
OTL App README.md에 Contributor 추가하기
3.
시간표가 하나만 남았을 때, 삭제 불가능 다이얼로그 띄우기
4.
만든 사람들 페이지를 단일 SVG에서 컴포넌트로 리팩터링
5.
과목 후기가 무한 로딩되는 버그 고치기
6.
Figma 디자인을 받아 졸업플래너 탭 구현하기
OTL 팀 Project Manager이신 조유민(yumyum)님과 협의하여, 팀 협업부터 코드 구조 파악, 컴포넌트 제작, 버그 수정, 그리고 마지막으로 디자이너와의 협업을 통한 신규 페이지 추가까지 점진적으로 큰 업무에 기여하실 수 있도록 설계하였습니다.
5월이 되어 마지막 과제에 다다르고, 정재현 님께서 졸업플래너 탭이 어느정도 개발이 완료되었다는 슬랙 메세지를 보내주셨고, 저는 테스트를 위해 Testflight로 앱을 빌드에서 팀 내부에 전파했습니다. 그런데 문제가 발생했습니다. Testflight를 통해 설치한 앱을 실행해 보니, 회색 화면밖에 나오지 않았던 것입니다.
로컬에서 빌드한 후 화면 @정재현
Testflight로 설치 후 화면 @조유민

Firebase Crashlytics를 통한 오류 원인 확인하기

릴리즈된 앱의 오류 분석을 위해 저희 OTL 팀은 Firebase Crashlytics를 사용하고 있습니다. 콘솔에서 앱 버전, 제가 앱을 사용한 시각과 플랫폼, iOS 버전 등을 대조해본 결과 아래 이벤트를 발견할 수 있었습니다.
OTL App의 Firebase Crashlytics 화면
세부 오류 로그 @오승빈
배포 과정에서 발생한 문제가 아니라, Flutter Widget 관련되어 발생한 문제임을 확인할 수 있었습니다. 더 자세한 디버깅을 위해서, 로컬 개발 환경에서 릴리즈 설정으로 빌드하여 테스트해 보기로 합니다.

Flutter 앱 로컬 개발 환경에서 릴리즈 빌드하기

이런 문제가 발생한 경우, 앱을 Debug 설정으로 빌드 했을때와 Release 설정으로 빌드 했을때 결과물에 차이가 있기 때문입니다. 따라서, 로컬 개발 환경에서 Release 설정으로 앱을 빌드하여 테스트해보아야 하며, 그 명령어는 아래와 같습니다.
flutter run --release
iOS 앱을 빌드할 때에는 위의 설정만으로 충분하지만, Android 앱의 경우 추가 설정이 필요합니다. android/app/build.gradle 파일에서 아래 signingConfigs와 buildTypes를 변경해야합니다.
android { .... signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } .... }
XML
복사
위 설정은 Release 빌드 과정에서 Google Play Store에 앱 번들을 업로드할 수 있도록 Upload용 인증서의 파일 위치와 비밀번호 등을 설정해 주는 부분인데요. 저희 팀에서는 Upload용 인증서와 CI용 인증서를 분리해서 관리하고 있기 때문에 아래 코드로 변경해 줍니다.
android { .... signingConfigs { release { keyAlias 'ci' keyPassword '123456' storeFile file('../ci.jks') storePassword '123456' } } .... }
XML
복사

문제의 원인

문제의 원인을 찾고 해결하는 모든 과정은 정재현(edge)님께서 수행해 주셨습니다.
실행한 후 터미널을 확인하면 아래 에러 로그를 확인할 수 있습니다.
I/flutter ( 9959): type 'StackParentData' is not a subtype of type 'FlexParentData' in type cast I/flutter ( 9959): #0 Flexible.applyParentData (package:flutter/src/widgets/basic.dart:5219) I/flutter ( 9959): #1 RenderObjectElement._updateParentData (package:flutter/src/widgets/framework.dart:6553) I/flutter ( 9959): #2 RenderObjectElement.attachRenderObject (package:flutter/src/widgets/framework.dart:6594) I/flutter ( 9959): #3 RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6458) I/flutter ( 9959): #4 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6900) I/flutter ( 9959): #5 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335)
XML
복사
결론은 Expanded widget을 잘못써서 생기는 에러였습니다. Stack Overflow에서 검색해보면, 아래 답변을 찾을 수 있습니다.
Expanded cannot be used inside a Stack.
You should use Expanded only within a ColumnRow or Flex
문제가 되었던 코드는 lib/pages/planner_page.dart 파일에서 OTLLayout의 body로 Expanded 위젯을 지정한 부분입니다.
class _PlannerPageState extends State<PlannerPage> { Widget build(BuildContext context) { final planners = Provider.of<PlannerModel>(context); if (!planners.isLoaded) { return OTLLayout( body: Expanded( ....
Dart
복사
왜냐하면 OTLLayout 위젯을 뜯어보면 body를 Stack이 감싸고 있거든요.
class _OTLLayoutState extends State<OTLLayout> { final bool canPopRightLeft = OTLNavigator.canPopRightLeft; final bool canPopDownUp = OTLNavigator.canPopDownUp; Widget build(BuildContext context) { final ScaffoldState? scaffold = Scaffold.maybeOf(context); final bool hasDrawer = scaffold?.hasDrawer ?? false; final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false; return Stack( fit: StackFit.expand, alignment: Alignment.topCenter, children: [ Positioned.fill( top: widget.extendBodyBehindAppBar ? 0 : kToolbarHeight, child: widget.body, ), ....
Dart
복사
재현님께서 원인 파악 후, 로딩 화면을 불러오는 로직과 위젯을 모두 수정하여 문제를 해결해 주셨습니다.

Wrap-up

개발자들을 놀라게 하는 이벤트 중 하나는 분명, 내 개발환경에서는 잘 돌아갔던 코드가 프로덕션/릴리즈 환경에서 작동하지 않는 상황일 것입니다. 우리는 이럴 때 두가지를 집중적으로 바라보아야 합니다.
릴리즈/프로덕션으로 배포하는 과정상의 문제인지? 코드 자체의 문제인지?
내 개발환경에서 릴리즈/프로덕션 설정을 모방하여 테스트해볼 수 있는지?
다행히 이번 상황에서는 Firebase Crashlytics를 통해 코드 관련 문제임을 빠르게 파악하고 담당자에게 전파할 수 있었던 것 같습니다.
감사합니다!