Repository navigation
마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1697 파일럿 #959
Description
#1697이 전체 마음의 기록 entry 계약으로 확장하기 전에 구현 비용과 함정을 검증하는 로컬 Nav3 파일럿이다. #924는 App Links 계약을, #1702는 최종 앱 루트 전환을 각각 소유한다.
왜 mindrecord 가 파일럿인가
전 그래프 중 유일하게 파일럿 조건을 충족한다 (커밋 104462fd 기준 실측).
| 그래프 | popUpTo |
그래프 스코프 공유 VM | 파일럿 적합 |
|---|---|---|---|
| mindrecord | 0 | 0 | ✅ |
| afternote(작성자) | 2 | AfternoteHostViewModel | ❌ 지문 게이트가 진입점 |
| onboarding | 3 | SignUp · FindId | ❌ |
| receiver | 4 | DeliveryVerificationFlow | ❌ nested flow VM |
| setting | 2 | SettingViewModel | ❌ |
| timeletter | 0 | TimeLetterWrite · Timeletter | ❌ |
순수 push/pop 뿐이라 Nav3의 최대 난관인 복잡한 그래프 스코프 VM 이관을 건드리지 않고 전환 비용만 측정할 수 있다.
범위
Route.MindRecord 안에 로컬 NavDisplay 백스택을 두고, 허브 내부 화면 3개를 그 안으로 옮긴다. 루트 NavHost(Nav2)에는 Route.MindRecord 하나만 남는다.
- 로컬 스택 편입: 허브(
HomeScreen) · 데일리질문 작성 · 일기 작성 · 임시저장 목록 -
MindRecordRoute를NavKey로 (+ 허브 키 추가) -
MindRecordNavActions8개 → 1개 (onMemorySpaceBack만 남음, push/pop 위임 7개 소멸) -
DiaryWriteViewModel라우트 인자 주입 경로 교체 (아래 함정 3) - 바텀바 판정에 로컬 스택 깊이 반영
-
Route.MemorySpace·Route.ReceiverMindRecord는 Nav2 잔류 — IA 상 허브 외부 진입점
선행 검증 (0823 실측 — 이 이슈 착수자가 다시 안 해도 되는 것)
전환 가능성 확인을 위해 프로토타입을 만들어 에뮬레이터(Pixel_7_Claude_QA, 실서버 QA823 계정)로 돌려봤고, 전 시나리오 통과했다.
| 시나리오 | 결과 |
|---|---|
| 허브 진입 · 카테고리 탭(데일리질문/일기/주간리포트) | ✅ |
| 작성 화면 push → 바텀바 숨김 | ✅ |
| 저장 성공 → pop → 허브 복귀 + 바텀바 재등장 | ✅ |
| 4단 스택 (허브→작성→임시저장 목록→이어쓰기) | ✅ |
| back 체인 pop | ✅ |
| 경계 back — 허브에서 back 이 루트 NavHost 로 전파 → 홈탭 | ✅ (#1702가 최종 루트에서 보존해야 할 최대 리스크) |
이어쓰기 인자(draftId) 프리필 |
✅ |
| pop 시 엔트리 VM clear (재진입 시 입력값 잔존 없음) | ✅ |
| 탭 이탈 후 복귀 — 허브·선택 카테고리 복원 | ✅ |
빌드 게이트도 통과: 컴파일 · :app:compileDebugUnitTestKotlin · mindrecord 유닛테스트 · konsist · ktlint.
미검증 1건: predictive back 미리보기 애니메이션 (원격 조작 한계 — 제스처 홀드 재현 불가). 실기기에서 확인 필요.
구현 함정 3가지 (실측으로 걸린 것)
1. NavDisplay.onBack 은 () -> Unit 이다
문서·블로그에 도는 onBack = { count -> ... } (Int 인자) 형태는 이 버전에서 컴파일 에러. onBack = { backStack.removeLastOrNull() }.
2. entryDecorators 를 넘기면 기본 목록을 통째로 대체한다
rememberSaveableStateHolderNavEntryDecorator() 를 직접 포함하지 않으면 rememberSaveable 상태 보존이 조용히 깨진다. VM 스코프용 rememberViewModelStoreNavEntryDecorator() 와 둘 다 넣어야 한다.
3. DiaryWriteViewModel — SavedStateHandle 라우트 주입이 사라진다
Nav3 엔트리에는 Nav2 의 savedStateHandle.toRoute<T>() 자동 채움이 없다. assisted 주입으로 바꾸면 타입 안전하게 대체된다:
@HiltViewModel(assistedFactory = DiaryWriteViewModel.Factory::class)
class DiaryWriteViewModel @AssistedInject constructor(
@Assisted private val route: MindRecordRoute.DiaryWriteRoute,
// …
) : ViewModel() {
@AssistedFactory
interface Factory { fun create(route: MindRecordRoute.DiaryWriteRoute): DiaryWriteViewModel }
}엔트리 쪽:
entry<MindRecordRoute.DiaryWriteRoute> { key ->
val viewModel = hiltViewModel<DiaryWriteViewModel, DiaryWriteViewModel.Factory>(
creationCallback = { factory -> factory.create(key) },
)
DiaryWriteScreen(viewModel = viewModel, /* … */)
}부수 효과로 DiaryWriteScreen 의 viewModel 파라미터가 기본값을 잃어 호출부가 주입해야 하고, androidTest 의 VM 직접 생성이 SavedStateHandle(mapOf("draftId" to …)) → route = MindRecordRoute.DiaryWriteRoute(draftId = …) 로 오히려 단순해진다 (MindRecordFlowAndroidTest · TimeLetterMindRecordCompletionAndroidTest 2곳).
바텀바: AppState.shouldShowBottomBar 는 Nav2 destination 만 보므로 로컬 스택 깊이를 모른다. 호스트가 onHubDepthChanged(isAtHub) 로 앱 셸에 올려 판정에 합성하고, 탭 이탈 시 DisposableEffect 로 true 복원 (다른 탭 판정 오염 방지).
의존성
navigation3 = "1.1.6" # 2026-08-12 stable
androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelKtx" }Nav3 는 1.0.0 stable 이 2025-11 출시, 현 stable 1.1.6. feature/mindrecord/presentation 에만 추가하고, 컨벤션 플러그인(afternote.android.navigation)의 Nav2 는 Route.MindRecord 루트 등록·라우트 직렬화에 계속 쓰이므로 공존한다.
충돌 지형 — 착수 시점 주의
착수 시점에 마음의 기록 열린 PR 이 많으면 (0823 기준 18건) MindRecordNavGraph · DiaryWriteViewModel · DiaryWriteScreen 이 광범위하게 바뀌므로 리베이스 비용이 크다. 열린 PR 을 먼저 정리한 뒤 착수하는 편이 낫다.
특히 #894 (홈 TODAY'S QUESTION CTA → 데일리질문 작성 화면 연결)는 작성 화면이 top-level 라우트라는 전제라 이 변경과 구조적으로 교차한다 — 홈에서 허브 내부 화면으로 바로 진입하는 경로는 로컬 스택 도입 후 초기 백스택 구성으로 다시 표현해야 한다.
완료 조건
- 위 실측 시나리오 9개 재현 + predictive back 확인
:app:compileDebugUnitTestKotlin포함 빌드 게이트 통과- #1697의 전체 entry 계약 이관 기준을 제공하며, 최종 결과는 #1702가 루트 전환에서 소비한다.
Activity
[-]refactor(mindrecord): 마음의 기록 허브를 Navigation 3 로컬 스택으로 — #924 1단계 파일럿[/-][+]마음의 기록 허브를 Navigation 3 로컬 스택으로 — #924 1단계 파일럿[/+][-]마음의 기록 허브를 Navigation 3 로컬 스택으로 — #924 1단계 파일럿[/-][+]마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1698 파일럿[/+][-]마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1698 파일럿[/-][+]마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1697 파일럿[/+]