Repository navigation
refactor(platform): MVI 아키텍처 전환 총괄 — ViewModel 51개 #1811
Copy link
Copy link
Closed as not planned
2 / 32 of 3 issues completedClosed as not planned
2 / 32 of 3 issues completed
Copy link
Labels
area:platformCI·빌드·릴리스·저장소 운영 영역CI·빌드·릴리스·저장소 운영 영역refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업동작 변경 없이 코드 구조·책임·경계를 개선하는 작업
Description
작업 유형
refactor — 동작 변경 없는 구조·책임·경계 개선
주 담당 모듈
platform — Android 앱·CI·빌드·릴리스·저장소 운영
개요
프레젠테이션 계층 전체를 MVI 로 전환하는 총괄 이슈. 실제 작업은 자식 이슈에서 한다.
도입하는 것
RuleUp-ASM/Android 의 MviViewModel 과 같은 계약이다. 흔히 «MVI» 라 불리는 것 중에서도 엄격한 쪽이고, 여기에 Intent / ReducerEvent 2단 분리가 더해져 있다.
abstract class MviViewModel<I : MviIntent, S : UiState, E : ReducerEvent>(
initialState: S,
) : ViewModel() {
abstract fun onIntent(intent: I) // 외부 진입점 하나
protected abstract fun reduce(state: S, event: E): S // 전이는 순수 함수로 강제
protected fun dispatch(event: E) // 상태 변경은 이 경로만
}
// 일회성 이벤트는 Channel 이 아니라 UiState 의 nullable 신호로 나른다 — 레포 정본 #228
Intent— 사용자가 하려는 것(SubmitDescription,SelectFilter,ConsumeSaveResult)ReducerEvent— 상태가 겪은 것(Loading,Loaded,DraftFailed)- Intent 하나가 ReducerEvent 를 0개에서 N개까지 낳는다. 네비게이션만 하는 Intent 는 0개, 로드 Intent 는
Loading→Loaded2개.
전환 규모 (develop 실측)
| 모듈 | ViewModel | Screen | on* 콜백 |
presentation @Test |
특이 |
|---|---|---|---|---|---|
| afternote | 9 | 12 | 37 | 246 | Consumed 3 · 에디터 29 fun |
| mindrecord | 10 | 10 | 64 | 232 | 이벤트 규약 없음 |
| setting | 11 | 17 | 88 | 46 | Channel 5 |
| timeletter | 7 | 7 | 45 | 20 | |
| receiver | 8 | 9 | 44 | 46 | Consumed 5 · onEvent 1 |
| onboarding | 3 | 9 | 53 | 23 | Consumed 3 |
| home | 1 | 1 | 1 | 16 | |
| app | 2 | — | — | — | |
| 합계 | 51 | 64 | 332 | 629 |
지금 무엇이 갈려 있나
- 단일
UiState노출은 이미 정착했다 — 32개 화면(sealed 19 / data 13). MVI 의 «Model» 축은 대체로 확보돼 있다. - 상태 전이 경로가 두 갈래다.
AfternoteEditorViewModel.mutateForm은 순수 리듀서에 가깝지만(withXxx가 전부copy()),ReceiverHomeViewModel은 코루틴 안에서_uiState.value = ...를 직접 쓴다. - 일회성 이벤트 소비 함수가 VM 마다 제각각 노출돼 있다. 규약(refactor: ViewModel 이벤트 → UI state 흡수 마이그레이션 (전수 9개 VM) #228)대로
UiState흡수를 쓰는 11개 VM(참조 77곳)이onXxxConsumed()를 각자 public fun 으로 열어 뒀고, 아직Channel인 5개 VM(setting)은 refactor(setting): 남은 이벤트 스트림 2곳을 UiState·MVI로 전환하고 재유입을 막는다 #1502 가 처리 중이다. - 단일 진입점은 51개 중 1개다.
ReceiverHomeViewModel.onEvent.
얻는 것
- 전이가
reduce한 함수에 모인다 - 리듀서 테스트가 코루틴 하네스 없이 순수 함수로 돌아간다 —
@Test629개가 지금 짊어진 비용의 상당 부분 - 일회성 신호의 소비 경로가
Intent.ConsumeXxx하나로 접힌다 — 신호를UiState에 흡수하는 규약(refactor: ViewModel 이벤트 → UI state 흡수 마이그레이션 (전수 9개 VM) #228)은 그대로 두고, VM 마다 흩어진onXxxConsumed()public fun 만 사라진다 - 화면이
Screen(stateful) /Content(internal stateless) 2단으로 갈려 프리뷰·screenshotTest 진입점이 명확해진다
진행 순서
#1811 전체 총괄
├─ #1812 core·platform·onboarding·receiver·afternote 몫 @1hyok
│ ├─ #1800 core 베이스 ← 세 역할 전부의 선행
│ ├─ #1801 konsist + 문서
│ ├─ #1802 onboarding 파일럿 ← 세 역할 전부의 선행 (관용구 확정)
│ ├─ #1803 receiver · #1804 afternote
│ ├─ #1809 잔여분 정리 (마지막)
│ └─ #1810 Nav3 접점 판정 — 코드 0줄
├─ #1813 setting·timeletter 몫 @koongmai
│ └─ #1805 setting · #1806 timeletter
└─ #1814 mindrecord·home 몫 @Sadturtleman
└─ #1807 mindrecord · #1808 home
역할별 총괄 아래에서만 착수한다. 담당자는 자기 총괄 하나만 보면 되고, 진행률도 역할 단위로 읽힌다.
#1809 에서 konsist allowlist 가 비고 Consumed 가 레포 전체 0건이 되면 전환이 끝난다.
#1800 과 #1802 는 세 역할 전부의 선행이다 — 이 둘이 늦어지면 나머지 두 역할이 통째로 막힌다. #1812 가 그 책임을 진다.
범위에서 뺀 것
- 화면 콜백 332개는 이 전환의 목표가 아니다. 대부분이 네비게이션인데, 그 축은 이미 Navigation 3 로컬 스택 이관이 담당하고 있다(마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1697 파일럿 #959 파일럿 · refactor(mindrecord): Navigation 3 파일럿을 전체 entry 계약으로 확장 #1697 전체 entry 계약 · refactor(setting): 설정 흐름을 Navigation 3 로컬 스택으로 이관 #1695 · refactor(timeletter): 타임레터 흐름을 Navigation 3 로컬 스택으로 이관 #1696 · refactor(platform): 소관 그래프를 Navigation 3 로컬 스택으로 이관 #1698 · TimeLetter 단순 back 위임 5개를 공통 명령으로 통합 #1301 back 위임 통합). MVI 전환은
Screen/Content2단 분리까지만 하고 콜백 개수를 건드리지 않는다. 두 축의 접점은 refactor(core): MVI 전환과 Navigation 3 이관의 접점 판정 — 네비게이션을 Intent 로 접을지 #1810 에서 판정한다. - 모듈별로 Nav3 이관이 병행 중이라 같은 파일을 건드린다. 각 전환 이슈 «참고» 란에 해당 모듈의 병행 이슈를 적어 뒀다 — 착수 순서는 담당자가 정한다. 겹침이 가장 큰 곳은 mindrecord(마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1697 파일럿 #959 · refactor(mindrecord): Navigation 3 파일럿을 전체 entry 계약으로 확장 #1697)와 setting(refactor(setting): 설정 흐름을 Navigation 3 로컬 스택으로 이관 #1695 · TimeLetter 단순 back 위임 5개를 공통 명령으로 통합 #1301)이다.
자식 이슈
- refactor(core): MVI 전환 — core·platform·onboarding·receiver·afternote 몫 총괄 #1812
- refactor(setting): MVI 전환 — setting·timeletter 몫 총괄 #1813
- refactor(mindrecord): MVI 전환 — mindrecord·home 몫 총괄 #1814
참고
참조 구현: RuleUp MviViewmodel.kt · HomeViewModel · HomeContract · HomeScreen
관련 선행 작업(closed): #283 (mutableStateOf → MutableStateFlow 통일) · #674 · #675 (에디터 폼 갱신 통로 재설계)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:platformCI·빌드·릴리스·저장소 운영 영역CI·빌드·릴리스·저장소 운영 영역refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업동작 변경 없이 코드 구조·책임·경계를 개선하는 작업
Activity
1 remaining item
OnlyReadOneBook commentedon Sep 13, 2026
실측 갱신 (origin/develop
7d87aa3f4, 2026-09-13)총괄이라 이 이슈 자체로 뗄 독립 범위가 없다. blockedBy 도 0건이다. 대신 본문의 수치와 소유자 표기가 지금 develop 과 어긋난 곳이 있어서, 다음 착수자가 묵은 전제로 시작하지 않도록 실측으로 갱신해 둔다. 코드 변경 0줄.
이미 develop 에 들어간 것
core/ui/src/main/kotlin/com/afternote/core/ui/mvi/MviViewModel.kt·MviContract.kt·ObserveSignal.ktkonsist/src/test/kotlin/com/afternote/konsist/MviContractKonsistTest.ktdocs/convention/mvi.md베이스와 가드와 문서는 끝났다. 남은 것은 모듈별 ViewModel 전환뿐이다.
1. 전환 규모는 51개가 아니라 52개
ReceiverHomeViewModel이 feature/home 으로 옮겨 갔다FindPasswordViewModel을 들여왔다재현:
2. 단일 진입점 ViewModel 의 소재와 소관이 바뀌었다
본문의 "단일 진입점은 51개 중 1개다.
ReceiverHomeViewModel.onEvent" 는 여전히 사실이지만 소재가 다르다.feature/home/presentation/src/main/kotlin/com/afternote/feature/home/presentation/receiver/ReceiverHomeViewModel.kt:86의fun onEvent(event: ReceiverHomeEvent)다.9f893a9f3) 다.MviContractKonsistTest.kt안의 주석은 이 이동을 refactor(home): 수신자 홈 재진입 갱신 이관과 home 정리 3건 (#1452) #1666 으로 적어 뒀는데, refactor(home): 수신자 홈 재진입 갱신 이관과 home 정리 3건 (#1452) #1666 은 수신자 홈 재진입 갱신을 다룬 다른 PR 이다. 주석도 정정 대상이다.HomeTabViewModel몫이라 이 VM 을 덮지 않는다.3. 종료 조건의 소유자는 #1809 가 아니라 #2022
본문 끝의 "#1809 에서 konsist allowlist 가 비고
Consumed가 레포 전체 0건이 되면 전환이 끝난다" 는 이제 #2022 다.| #1809 | 마무리 | app VM 2 + konsist allowlist 종료 |줄도 같은 정정 대상이다.4. konsist allowlist 가 비어도 app 모듈은 안 덮인다
종료 검사(#2022)를 맡는 쪽이 먼저 알아야 할 것. 규칙 B 의 위반 후보가 경로로 한 번 걸러진다.
실측하면 이렇게 갈린다.
PENDING_MVI_MIGRATION은 항목 51개다. 그중 실소스에 없는 스테일이 딱 1개,com.afternote.feature.receiver.presentation.home.ReceiverHomeViewModel이다. PR refactor(receiver): 구 수신자 흐름 MVI 전환 철회 (#1803) #2039 가ISSUE_1803_RECEIVER블록째로 지운다.com.afternote.afternote_fe.MainViewModel,com.afternote.afternote_fe.notification.NotificationPermissionViewModel)는 저 정규식 밖이라 allowlist 에 아예 없고 가드도 보지 않는다. 즉 allowlist 가 비는 것만으로 전환이 끝났다고 판정하면 이 둘이 조용히 빠진다.NotificationPermissionViewModel은 refactor(platform): 알림 권한 ViewModel과 Effect를 MVI로 전환 #1809 (PR refactor(app): 알림 권한 상태·결과를 MVI로 전환 (#1809) #2033) 가 가져갔고,MainViewModel은 refactor(platform): 최종 앱 루트 책임 판정과 MVI 전환 종료 검사 (#1809 분리) #2022 가 "전환할지 말지를 먼저 판정하고, 아니면 근거를 남긴 뒤 allowlist 에 영구 예외로 등재한다" 로 들고 있다. 다만 그 영구 예외라는 처방이 성립하려면 가드가 app 모듈을 보고 있어야 하는데, 지금 정규식으로는 보지 않는다. refactor(platform): 최종 앱 루트 책임 판정과 MVI 전환 종료 검사 (#1809 분리) #2022 에서 같이 판정할 일이다.5.
onXxxConsumed실측본문의 "11개 VM(참조 77곳)" 은 develop 에서 이렇게 된다. #2022 체크리스트에도 같은 수치가 복사돼 있어 함께 정정 대상이다.
fun on*Consumed선언재현:
참조 수에서 KDoc 안의 플레이스홀더
onXxxConsumed3건(MviContract.kt,OnboardingProfileEntry.kt,SignUpUiState.kt)은 뺐다. 실제 심볼이 아니라 설명문이다.setting 의
Channel5곳은 본문 그대로 맞다. VM 기준으로도 5개이고 #1502 몫이다.6. 총괄 트리에 안 걸린 MVI 이슈가 8건 있다
GraphQL
parent가 NONE 인 것들이다. 이 8건이 어느 역할 총괄에도 안 달려 있어서 #1812 진행률이 2/7 로 실제보다 적게 읽힌다.ReceiverHomeViewModel과 수신자 홈 화면붙일 자리는 제안이고 아직 아무것도 걸지 않았다. #1811·#1812·#1813 은 1hyok 소유라 그 밑 5건은 이쪽에서 sub-issue 로 걸 수 있다. #1814 는 Sadturtleman 소유라 그 밑 3건은 담당자가 직접 걸도록 목록으로만 남긴다.
7. 진행 순서 트리의 담당자 표기가 실제와 다르다
본문 "진행 순서" 트리와 GitHub assignee 가 어긋난 곳이 둘이다.
8. 임계경로는 #1802 하나로 좁혀졌다
지금 열려 있는 MVI PR
이 이슈에서 손댈 코드가 없다고 판정한 근거이기도 하다.
MviContractKonsistTest.kt한 파일을 열린 PR 6건이 동시에 물고 있다.refactor/1863-onboarding-failuredeveloprefactor/1902-memorial-photorefactor/1804-editor-mvirefactor/1502-setting-state-mvicodex/1695-setting-nav3developdevelopdocs/convention/mvi.md와core/ui/.../MviContract.kt는 #1825 와 #2038 이 물고 있다. 남은 모듈(mindrecord·timeletter·home)은 다른 담당자 몫이다. 이 상태에서 총괄이 코드를 새로 쓰면 충돌만 만들고, 소비처 없는 계약이 하나 더 생긴다.다음 착수자에게
OnlyReadOneBook commentedon Sep 23, 2026
하위 이슈만 묶어 두는 총괄 이슈는 두지 않기로 정리하면서 닫습니다(2026-09-23). 이 이슈에는 자기 코드 몫이 없고, 남은 전환은 담당자별 이슈가 각자 진행합니다.
선행 관계(#1800·#1802 기반)는 하위 이슈에 blocked_by 로 걸려 있습니다. 위 전환 규모 표와 계약 설명은 이 본문에 그대로 남겨 둡니다.