콘텐츠로 이동

8. 마무리와 도전과제

축하합니다. 빈 프로젝트에서 시작해 진짜 지도, 진짜 검색, 진짜 저장이 되는 앱을 끝까지 만들었습니다. 이 장에서는 새 코드를 거의 쓰지 않습니다. 대신 우리가 만든 것의 전체 그림을 한 발 떨어져서 보고, 스스로 앱을 키워 나갈 도전과제를 남깁니다.

완성된 버킷맵: 지도 위에 다녀온 경복궁이 초록 마커, 가보고 싶은 덕수궁이 주황 마커로 표시되어 있다

우리가 만든 것 — 전체 구조 복습

버킷맵의 부품 전체를 한 장에 그리면 이렇다.

        [ 화면 4개 ]
┌──────────────┐   장소 찾기   ┌────────────────┐   장소 탭   ┌──────────────────────┐
│ MainActivity │ ───────────▶ │ SearchActivity │ ─────────▶ │ PlaceDetailActivity  │
│  지도 + 마커 │              │  검색 + 목록   │            │  상세 + 찜/도장/빼기 │
└──────┬───────┘              └───────┬────────┘            └──────────┬───────────┘
       │ 내 장소                      │                                │
       ▼                             │ 인터넷                          │ 저장/수정/삭제
┌──────────────────┐                 ▼                                ▼
│ MyPlacesActivity │        ┌─────────────────┐              ┌─────────────────┐
│  가볼 곳/다녀온 곳│        │ [ 인터넷 부품 ] │              │ [ 저장소 부품 ] │
└────────┬─────────┘        │ PlacesApi       │              │ SavedPlace      │
         │ 장소 탭          │ ApiClient       │              │ SavedPlaceDao   │
         └──────────▶ 상세  │ SearchRequest   │              │ AppDatabase     │
                            │ SearchResponse  │              │  (bucketmap.db) │
                            │  → Place 로 변환│              └─────────────────┘
                            └─────────────────┘

화면 사이의 이동은 전부 2장에서 배운 명시적 인텐트이고, 화면 사이의 데이터 전달은 전부 extra 이다.

인터넷 부품은 6장의 Retrofit, 저장소 부품은 7장의 Room 이다.

우리가 만든 파일 전부를 역할 한 줄씩으로 정리하면 이렇다. 파일 이름만 보고 역할이 떠오르면 이 과정을 완전히 소화한 것이다.

파일 역할 만든 장
MainActivity.java 지도 + 버킷리스트 마커 + 두 화면으로 가는 현관 2·5·7장
SearchActivity.java 검색어 입력 → API 호출 → 결과 목록 2·4·6장
PlaceDetailActivity.java 장소 상세 + 찜/도장/빼기 2·3·7장
MyPlacesActivity.java 저장한 장소를 칩으로 걸러 보여 주는 목록 2·4·7장
SearchResultAdapter.java 검색 결과(Place)를 목록 줄로 그리는 어댑터 4장
SavedPlaceAdapter.java 저장 장소(SavedPlace)를 목록 줄로 그리는 어댑터 7장
Place.java 검색 결과 한 곳을 담는 우리 앱의 모델 4장
PlacesApi.java "서버에 이런 요청을 보낸다"는 Retrofit 선언 6장
ApiClient.java Retrofit 을 하나만 만들어 나눠 주는 창구 + 사진 주소 조립 6장
SearchRequest.java 서버로 보내는 요청 몸체 (JSON 이 될 모양) 6장
SearchResponse.java 서버가 돌려주는 응답 (JSON 에서 올 모양) 6장
SavedPlace.java 버킷리스트 한 곳 = 데이터베이스 표의 한 줄 7장
SavedPlaceDao.java 넣고/꺼내고/고치고/지우는 방법 선언 7장
AppDatabase.java 데이터베이스 본체 (bucketmap.db) 7장

이번에는 데이터 하나가 앱을 통과하는 여행을 따라가 보자. 검색어 "palace" 가 주황 마커가 되기까지:

  1. SearchActivity 가 검색어를 SearchRequest 객체에 담고, Retrofit 이 그것을 JSON 으로 바꿔 구글 서버로 보낸다.
  2. 구글 서버가 JSON 으로 응답하면, Gson 이 그것을 SearchResponse 객체로 바꿔 준다.
  3. toPlaces() 가 바깥 세상의 모양(SearchResponse)을 우리 앱의 모양(Place)으로 변환하고, 어댑터가 목록으로 그린다.
  4. 목록에서 한 줄을 탭하면 Place 의 값들이 extra 에 실려 PlaceDetailActivity 로 이동한다.
  5. [가보고 싶어요] 를 누르면 extra 값들이 SavedPlace 로 조립되어 Room 을 통해 bucketmap.db 파일에 저장된다.
  6. 지도로 돌아오면 MainActivityonResume() 이 DB 를 다시 읽고(findAll), 상태에 맞는 색의 마커로 그린다.

이 여행에서 알 수 있듯, 좋은 앱은 "화면"과 "데이터"가 분리되어 있다.

화면은 데이터를 보여 줄 뿐이고, 진실은 항상 데이터베이스에 있다.

그래서 어느 화면에서 무엇을 바꾸든 — 다른 화면들은 onResume() 에서 DB 를 다시 읽는 것만으로 저절로 맞는 모습이 된다.

완성 점검 체크리스트

배포 전의 개발자처럼, 앱의 전체 동선을 순서대로 점검해 봅니다. 하나라도 다르게 동작하면 해당 챕터로 돌아가 코드를 대조하십시오.

# 해보기 이렇게 되면 성공
1 앱 실행 지도가 뜨고, 위치 권한 허용 시 내 위치로 이동한다 (5장)
2 [장소 찾기] → 검색어 입력 → [검색] 진짜 장소 목록이 나온다 (6장)
3 장소 탭 사진·주소·평점이 있는 상세 화면이 열린다 (6장)
4 [가보고 싶어요] 토스트가 뜨고 버튼이 [다녀왔어요][빼기] 로 바뀐다 (7장)
5 지도로 복귀 주황 마커가 생겼다 (7장)
6 주황 마커 탭 → [다녀왔어요] 상태가 "다녀온 곳"이 되고, 지도의 마커가 초록으로 바뀐다 (7장)
7 [내 장소] → 칩 전환 [가볼 곳]/[다녀온 곳] 목록이 각각 맞게 나온다 (7장)
8 상세에서 [빼기] 마커와 목록에서 사라진다 (7장)
9 앱 완전 종료 후 재실행 저장한 장소가 그대로 남아 있다 (7장)

도전과제

여기서부터는 정답 코드가 없습니다. 지금까지 배운 부품만으로 전부 만들 수 있습니다 — 막히면 해당 챕터를 다시 펴 보십시오. 스스로 만든 기능이 하나 늘 때마다, 이 앱은 점점 "따라 만든 앱"이 아니라 여러분의 앱이 됩니다.

[self Test 1] - 내 장소에서 고른 곳을 "지도에서 보기"

상세 화면에 [지도에서 보기] 버튼을 추가하고, 누르면 메인 지도가 그 장소로 이동하게 만들어 보시오.

  • (힌트) 마커는 이미 지도에 있으니, 카메라만 옮기면 된다. 5장의 CameraUpdate.scrollTo() 를 떠올릴 것.
  • (힌트) 메인으로 "돌아가면서" 데이터를 전하는 방법이 고민이라면 — Intent 를 새로 만들어 MainActivity 로 보내고, onCreate 또는 onNewIntent 에서 extra 를 읽는 방법을 찾아볼 것.

[self Test 2] - 최근 검색어

검색 화면을 열면 최근에 검색했던 검색어들이 보이게 만들어 보시오.

  • (힌트) 검색어도 저장할 데이터이다 — 두 번째 @Entity 클래스와 그 DAO 를 만들고, AppDatabaseentities 배열에 추가한다.
  • (힌트) 표의 구성을 바꾸면 데이터베이스 version 을 올리고 이사 계획(마이그레이션)을 짜야 하는데, 그것은 아직 배우지 않았다. 학습 단계에서는 앱을 삭제하고 다시 설치하면 데이터베이스가 새로 만들어진다 — 개발자들이 실제로 쓰는 우회법이다.

[self Test 3] - 친구에게 장소 공유하기

상세 화면에 [공유] 버튼을 추가하고, 누르면 장소 이름과 주소를 메시지 앱 등으로 보낼 수 있게 만들어 보시오.

  • (힌트) 지금까지 쓴 인텐트는 "어느 화면으로 갈지"를 콕 집는 명시적 인텐트였다. 공유는 반대로 "이 일을 할 수 있는 앱 아무나"를 부르는 암시적 인텐트를 쓴다 — Intent.ACTION_SEND 를 검색해 볼 것.

[self Test 4] - 데이터베이스를 메인 스레드 밖으로

7장의 경고 박스를 기억하는가? allowMainThreadQueries() 를 지우고도 앱이 돌아가게 만들어 보시오.

  • (힌트) Executors.newSingleThreadExecutor() 로 일꾼 스레드를 하나 만들어 DB 작업을 맡긴다.
  • (힌트) 일꾼 스레드에서는 화면을 만질 수 없다 — 결과를 화면에 반영할 때는 runOnUiThread(...) 로 감싼다. 6장에서 Retrofit 의 enqueue 가 해 주던 일을 이번에는 직접 하는 것이다.

[self Test 5] - 방문 소감 메모

다녀온 장소에 "여기 최고!" 같은 한 줄 소감을 남길 수 있게 만들어 보시오.

  • (힌트) SavedPlacememo 필드를 추가하고, 상세 화면에 EditText 와 저장 버튼을 단다. DAO 에는 memo 만 고치는 @Query("UPDATE ...") 를 하나 추가한다.
  • (힌트) 이것도 표의 구성이 바뀌는 일이다 — self Test 2 와 같은 이유로 앱 삭제 후 재설치가 필요하다.

앱을 세상에 내놓으려면 — 배포 소개

지금 버킷맵은 에뮬레이터와 내 폰에서만 돌아갑니다. 다른 사람들이 설치하게 하려면 Play 스토어에 배포하는 과정을 거친다.

  1. Android Studio 의 Build > Generate Signed App Bundle / APK 로 서명된 AAB 파일을 만든다 — 서명은 "이 앱은 내가 만들었다"는 도장이다.
  2. Google Play Console 에 개발자 등록을 하고(1회 등록비 필요) AAB 를 올린 뒤 심사를 받는다.

배포는 그 자체로 한 과정이 될 만큼 내용이 많아, 이 과정에서는 여기까지만 소개합니다. 중요한 것은 — 여러분이 만든 이 앱은 기술적으로는 이미 스토어에 올릴 수 있는 완성품이라는 사실입니다.

다음 단계

더 공부하고 싶은 사람을 위한 예고편입니다.

  • ViewBinding — 우리는 화면 부품을 전부 findViewById 로 찾았다. 화면이 커지면 이 호출이 수십 개가 되는데, ViewBinding 을 켜면 안드로이드가 부품들을 찾아 둔 클래스를 대신 만들어 준다.
  • 화면 회전과 ViewModel — 검색 결과가 나온 상태에서 에뮬레이터를 가로로 돌려 보면, 결과가 사라진다. 회전할 때 안드로이드가 액티비티를 부수고 새로 만들기 때문이다. 회전에도 데이터를 살려 두는 전용 보관소가 ViewModel 이고, 여기서부터가 실무 앱 구조(아키텍처)의 세계이다.

마치며

3장에서 색 하나 바꾸고 기뻐하던 앱이, 이제 인터넷과 대화하고 데이터베이스를 다루는 앱이 되었습니다. 이 과정에서 여러분이 정말로 배운 것은 지도나 검색이 아니라 — 큰 문제를 챕터 하나 크기로 쪼개서, 매번 실행되는 상태를 유지하며 나아가는 방법입니다. 다음에 만들고 싶은 앱이 생기면, 이번처럼 화면부터 그리고, 부품을 하나씩 붙여 나가면 됩니다.

버킷맵의 첫 번째 목적지로, 여러분은 어디를 담겠습니까? 🧡