티스토리 뷰
Flutter 연습용으로 포켓몬 도감을 만들어봤다. 총 개발 기간은 자료 수집 + UX/UI + 개발까지 하루.
코드: https://github.com/yousung/flutter_poketmon
앱 특징
- 무료
PokeAPI가 생각보다 다루기 까다로웠다
개발 후기를 한 줄로 요약하면 이렇다. PokeAPI가 생각보다 구조적으로 좋은 구조가 아니어서 어려움을 겪었다.
포켓몬 리스트를 뽑고 싶은데, 리스트 응답에는 한글 이름이 없거나 이미지가 없는 식이었다. 구체적으로 어떤 구조인지 정리해둔다.
1. 목록 응답에 이름과 URL만 있다
GET /api/v2/pokemon?limit=20
{
"results": [
{ "name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/" },
{ "name": "ivysaur", "url": "https://pokeapi.co/api/v2/pokemon/2/" }
]
}
목록만으로는 화면을 그릴 수 없다. 이미지도 타입도 없다. 결국 목록 20개를 받으면 상세를 20번 더 호출해야 한다. 전형적인 N+1이다.
2. 한글 이름은 다른 엔드포인트에 있다
/pokemon/{id} 의 name은 영문뿐이다. 한글 이름을 얻으려면 /pokemon-species/{id} 를 따로 호출해서 names 배열에서 language.name === 'ko' 인 항목을 찾아야 한다.
GET /api/v2/pokemon-species/1
{
"names": [
{ "language": { "name": "ko" }, "name": "이상해씨" },
{ "language": { "name": "en" }, "name": "Bulbasaur" }
]
}
즉 포켓몬 한 마리를 온전히 그리려면 최소 두 번의 요청이 필요하다. 20마리면 41번이다.
3. 이미지도 여러 경로에 흩어져 있다
sprites 안에 도트 이미지가 있는데 해상도가 낮다. 고화질 아트워크는 sprites.other['official-artwork'].front_default 처럼 중첩된 경로에 있고, 포켓몬에 따라 null인 경우도 있다.
결국 이미지 URL은 규칙이 있어서 직접 조합하는 편이 안정적이었다.
해결 방법
병렬 요청으로 묶기
상세를 순차로 호출하면 20마리에 몇 초씩 걸린다. Future.wait으로 병렬 처리하면 체감이 확 달라진다.
Future<List<Pokemon>> fetchPage(int offset) async {
final list = await api.getList(offset: offset, limit: 20);
final futures = list.results.map((item) async {
final detail = await api.getDetail(item.url);
final species = await api.getSpecies(detail.id);
return Pokemon.from(detail, species);
});
return Future.wait(futures);
}
캐싱
포켓몬 데이터는 거의 바뀌지 않는다. 한 번 받으면 로컬에 저장해두고 다음부터는 네트워크를 타지 않게 하는 게 맞다. Hive나 shared_preferences로 충분하다.
GetX를 써본 소감
상태 관리는 GetX + http 조합으로 개발했다.
빠르게 만들기에는 확실히 편했다. 상태 관리, 라우팅, 의존성 주입, 다국어까지 하나의 패키지로 해결된다. 하루 만에 만든다는 목표에는 잘 맞았다.
class PokemonController extends GetxController {
final pokemons = <Pokemon>[].obs;
final isLoading = false.obs;
Future<void> load() async {
isLoading.value = true;
pokemons.value = await repository.fetchPage(0);
isLoading.value = false;
}
}
// 뷰
Obx(() => controller.isLoading.value
? const CircularProgressIndicator()
: PokemonGrid(items: controller.pokemons))
다만 아쉬운 점도 있었다.
- 전역 접근이 너무 쉽다.
Get.find()로 어디서든 컨트롤러를 꺼낼 수 있으니 의존 관계가 코드에 드러나지 않는다. 프로젝트가 커지면 어디서 무엇을 쓰는지 추적이 어려워진다 - 테스트가 불편하다. 전역 상태에 의존하는 구조라 유닛 테스트에서 격리하기가 번거롭다
- Flutter 표준에서 벗어난다.
BuildContext없이 라우팅과 스낵바를 띄우는 건 편하지만, 프레임워크가 의도한 방식은 아니다
그래서 다음번에는 GetX를 걷어내고 Bloc으로 상태 관리를 해보려고 한다. 보일러플레이트가 늘어나는 대신 이벤트–상태 흐름이 명시적이라 규모가 커졌을 때 유리하다는 얘기를 많이 들었다. 직접 비교해보는 게 목적이다.
이 프로젝트에서 얻은 것
- API 설계를 소비자 입장에서 보게 됐다. 목록에 필요한 최소 정보를 안 주면 클라이언트가 N+1을 떠안는다. 내가 API를 만들 때 참고할 지점이다
- Flutter의 비동기 + 리스트 렌더링 패턴을 한 사이클 돌려봤다
- 상태 관리 라이브러리 선택에는 트레이드오프가 있다. "빠른 개발"과 "구조적 명확성"은 대개 반대 방향이다
정리
- Flutter + GetX + http로 하루 만에 제작
- PokeAPI는 목록에 이미지·한글명이 없어서 포켓몬당 최소 2회 요청이 필요하다
Future.wait병렬 처리와 로컬 캐싱이 사실상 필수- GetX는 빠르지만 전역 접근과 테스트에서 아쉬움이 있다
- 다음에는 Bloc으로 다시 만들어볼 계획
- 코드: github.com/yousung/flutter_poketmon
'Toy Project' 카테고리의 다른 글
| 키즈랜드 (0) | 2019.01.27 |
|---|---|
| 급식급식 (급식 조회 어플) - (진행중) (0) | 2019.01.11 |
| 클린에어 (미세먼지) - 보류 (0) | 2019.01.11 |
