Flutter에 관하여서 막연한 관심만 가지고 있다가
직접 경험할 수 있는 좋은 기회가 되어 2개의 앱을 개발하였고 론칭하게 되었습니다.
아무것도 몰랐던 바닥부터 Flutter에 대하여 공부하고, 조사하고
런칭하게 되기까지 개발 후기 2편에 대하여 적어보고자 합니다 🚀
[앱 개발 후기] Flutter로 2개의 앱을 개발하다 1편 보러 가기
beomseok95.tistory.com/322
Index
Bleet(블릿)
Flutter 두 번째 앱 Bleet 개발 시작
Flutter로 개발한 첫 번째 앱이 첫 배포를 마치자마자
곧바로 두 번째 앱 개발을 시작하게 되었습니다.
한해에 두 개나 신규 앱을 만들 수 있다는 생각에 매우 들뜨고 두근두근거렸었고
첫 번째 앱에서 아쉬웠던 점을 새로 보완하고자
다시 여러 가지 조사를 시작하였습니다.
아쉬웠던 점 보완하기
첫 번째 앱을 만들면서 아쉬웠던 점들을 보완하기 위해
여러 방안을 찾아보았습니다.
🟧 Flutter의 장점 중 하나인 애니메이션을 잘 살리지 못했던 점
애니메이션을 구현하는데 지식이 부족하였던 부분들을 다시 공부하고 정리
beomseok95.tistory.com/320
다른 여러 사람들이 구현해놓은 애니메이션 조사
www.notion.so/6ccfcada558142a1aa066cfedd59d0cd
🟦 중구난방 리소스
기존 앱에서 여러 하드 코딩된 문자열, 이미지 등을 미리 정리하고자 하였고
문자열은 따로 정리하고, 이미지는 Generate 하는 방식의 spider를 이용하였습니다.
pub.dev/packages/spider
🟥 미완성 아키텍처
기존 아키텍처가 MVP+MVVM+Provider를 사용하여
변형하여 사용하였어서 테스트를 작성하기에는 좋았지만
UI클래스를 따로 관리해야 하였고, 직접 notifyListeners()를 호출하여야만
위젯의 상태가 변경되었어서, ViewModel이라는 명칭을 사용하고 있었지만
과연 MVVM이라고 할 수 있을까??라는 의문이 들었었고 사용하는 이점이 줄어든다고 생각하였습니다.
그래서 새로 프로젝트를 시작하는 만큼 많은 변화를 주고자 하였습니다.
그래서 팀원분들과 상의하여 정석 MVVM패턴으로 메인 아키텍처로 변경하기로 하였습니다.
MVVM 아키텍처 적용하기
MVVM을 메인 아키텍처로 적용하기 위해 여러 레퍼런스를 찾아보았고
MVVM패턴의 특징을 제대로 적용하기 위하여
Rxdart와 Stream을 사용하기로 결정하였습니다.
MVVM패턴의 특징
🔶 ViewModel은 View와 Model 사이의 매개체 역할만
🔷 Model에서 제공받은 데이터를 UI에서 필요한 정보로 가공한 뒤 View가 가져갈 수 있게 데이터 변경에 대한 "이벤트"만 보냄
🔶 ViewModel과 View는 MVP패턴과 다르게 Many to One의 관계를 가질 수 있음
🔷 View는 ViewModel의 reference를 가지지만 ViewModel은 View에 대한 정보가 전혀 없어야 한다
Rxdart와 Stream을 사용하면
ViewModel은 정보를 가공하여 View(위젯)에 이벤트만 전달하고
View(위젯)은 ViewModel의 Stream을 구독하여
StreamBuilder로 위젯에서 상태를 자유롭게 처리할 수 있기 때문에 Many to One 특징을 잘 살릴 수 있었습니다.
예) viewmodel
ViewModel에서 repository를 통해 목록을 가져오고 정보를 가공한 뒤
list라는 Subject에 넣어주게 되고 Widget에서는 StreamBuilder를 통해 위젯을 갱신하게 됩니다.
@injectable
class LoungeListViewModel extends BaseViewModel {
final LoungeRepository repository;
LoungeListViewModel({this.repository});
final type = BehaviorSubject<LoungeType>.seeded(LoungeType.ALL);
final status = BehaviorSubject<LoungeStatus>.seeded(LoungeStatus.ALL);
final sort = BehaviorSubject<LoungeSortType>.seeded(LoungeSortType.LATEST);
final list = BehaviorSubject<List<LoungeEntity>>.seeded(const []);
var offset = 0;
Stream<List<LoungeEntity>> load() {
offset = 0;
return repository
.getLoungeList(
page: offset,
type: type.value,
status: status.value,
sort: sort.value,
)
.map((res) => res.loungeList)
.doOnData((l) => list.add(l))
.handleError(onError);
}
@override
void dispose() {
type?.close();
status?.close();
sort?.close();
list?.close();
super.dispose();
}
}
예) viewmodel_test
테스트를 작성할 때는 Mockito를 사용하였으며
예를 들어 목록 로드가 성공하였을 때를 테스트할 때
☑️ 성공 응답을 미리 설정하고(Given)
☑️ load함수를 호출(When)
☑️ expectLater 함수로 비동기 결과에 대한 검증(Then)
하도록 케스트를 작성하였습니다.
main() {
MockLoungeRepository repository;
LoungeListViewModel viewModel;
setUp(() {
repository = MockLoungeRepository();
viewModel = LoungeListViewModel(repository: repository);
});
final list = List.generate(20, (index) => LoungeParser()).toList();
final response = LoungeListResponseEntity(
totalCount: CountParser(count: 1000),
loungeList: list,
);
final errorMessage = '일시적인 접속불가입니다.';
test('목록 로드 성공', () async {
// given
when(repository.getLoungeList(
page: 0,
type: LoungeType.ALL,
status: LoungeStatus.ALL,
sort: LoungeSortType.LATEST,
)).thenAnswer((_) => Stream.value(response));
// when
final result = viewModel.load();
// then
await expectLater(result, emitsInOrder([list]));
await expectLater(viewModel.list, emitsInOrder([list]));
expect(viewModel.offset, 0);
verify(repository.getLoungeList(
page: 0,
type: LoungeType.ALL,
status: LoungeStatus.ALL,
sort: LoungeSortType.LATEST,
));
}, timeout: Timeout(Duration(seconds: 5)));
test('목록 로드 실패', () async {
// given
when(repository.getLoungeList(
page: 0,
type: LoungeType.ALL,
status: LoungeStatus.ALL,
sort: LoungeSortType.LATEST,
)).thenAnswer(
(_) => Stream.error(
PayloadException(
code: ErrorCode.INVALID_AUTH_TOKEN,
message: errorMessage,
),
),
);
// when
viewModel.load().listen((event) {});
// then
await expectLater(
viewModel.event,
emitsInOrder([BaseEvent.showToast(errorMessage)]),
);
expect(viewModel.offset, 0);
expect(viewModel.totalCount, 0);
verify(repository.getLoungeList(
page: 0,
type: LoungeType.ALL,
status: LoungeStatus.ALL,
sort: LoungeSortType.LATEST,
));
}, timeout: Timeout(Duration(seconds: 5)));
}
개발을 진행하며..
두 번째로 앱 개발을 진행하다 보니 손에 익어 개발 속도가 더 빠르고 쉬웠습니다.
첫 번째 앱이 웹뷰로 인해서 골머리를 앓았던 반면, 웹뷰가 거의 포함되어있지 않은 기획이어서
딱 Flutter로 개발하는 것이 좋은 앱이었습니다.
채팅 모듈을 제외하고 거의 순탄하게 개발이 진행되어서
약 2달이 걸려 앱 개발이 완료되었고 1달간의 QA후에 심사요청을 하게 되었습니다
하지만..
첫 심사요청을 한 뒤 약 2주가 걸릴 동안 4번을 리젝 당하게 되었고.. (애플 로그인 관련)
첫 번째 앱을 만들 당시에는 쉽게 그나마 넘어갔던 것임을 느끼게 되었고 (애플이 애플 했다😥)
심사 통과 후 11월에 첫 번째 배포를 하게 되었습니다
Flutter앱 개발 후기
장점
🔶 러닝 커브가 낮다
1달 정도 공부하면 누구나 어렵지 않게 개발할 수 있을 만큼 쉽게 개발이 가능합니다.
🔷 크로스 플랫폼의 장점
Android, iOS를 동시에 하나의 소스를 기반으로 개발할 수 있으므로 개인 앱 개발에 최고고
실 서비스에서도 운영 시에 유지 보수를 위한 인원이 1명만 투입되면 되기 때문에 개발 비용 절감(?!) 효과도 있을 것입니다.
🔶 핫 리로드를 통한 빠른 개발
핫 리로드는 JIT 컴파일러를 통해 프로그램 즉시 컴파일이 가능한 것인데
이는 코드 수정 때마다 발생하는 빌드 시간이 크게 줄어 빠르고 편하게 개발이 가능하였습니다.
(개인적으로 굉장히 만족했던 부분)
단점
🔶 플러그인
플러그인이 대부분 1 이하 버전이어서 불안정한 부분이 많습니다
또한 Flutter프레임워크 개발이 활발히 진행 중이어서 플러그인이 이를 못 따라가는 경우도 있습니다.
현재 Flutter Github의 등록된 이슈가 5K+ 개 등록되어있어 안정화가 되려면 시간이 많이 필요할 것 같습니다.
🔷 아키텍처와 같은 레퍼런스가 적음
기존의 Android, IOS에서는 대세를 이루는 Architrcture들이 존재했습니다.
Android와 같은 경우에는 (MVP, MVVM, Clear Architecture) 등이 IOS와 같은 경우에는 (MVC, MVVM, Viper)등이 있습니다
하지만 Flutter는 아직 이렇다 할만한 아키텍처가 없습니다. 그 외에도 개발 컨밴션적인 부분이나 규칙들 또한 아직 많이 미약합니다.
따라서 Flutter를 개발할 때는 아키텍처와 규칙들을 직접 신중하게 골라서 개발하셔야 합니다.
맺으며
2020년은 거의 Flutter개발로 보낸 한 해였었고
Codelabs Tutorial 만해보던 Flutter를 본격적으로 공부를 시작하고
실제로 서비스를 두 개나 만들어본 해라는 점에서 많이 뿌듯하기도 하였습니다.
아직 Flutter는 불완전하다는 주위의 염려도 있었지만
Flutter는 실제로 개발해 보았을 때 좋다고 말하고 싶습니다.
혹시나 Flutter 도입을 고민하고 있으시다면 개발하고자 하는 서비스에
필요한 기능들이 구현이 가능한지 꼭 한번 검토해보고 개발을 시작하는 것이 좋을 것 같습니다.
플러터를 배우면서 참 재미있고 장점이 많다고 느꼈고, 앞으로 어떻게 발전해갈지 매우 궁금합니다 ㅎㅎ
이다음에는 Flutter로 웹을 만들기를 도전해보아야지..ㅎㅎ 다음에 또 다른 포스팅으로 찾아뵙겠습니다
끝가지 읽어주셔서 감사합니다🙇
앱 구경하러 가기( Mybiskit & Bleet)
'Daily' 카테고리의 다른 글
2020 회고 및 2021 신년계획 (11) | 2021.01.13 |
---|---|
[앱개발후기] Flutter로 2개의 앱을 개발하다 (1편) (24) | 2020.12.30 |
댓글