feat: execute route and release recovery contracts

This commit is contained in:
donghyeon-ka
2026-07-26 14:26:39 +09:00
parent a33e93d4d4
commit ce0040e407
49 changed files with 1761 additions and 373 deletions
@@ -0,0 +1,58 @@
# VD-03: React Router Data Mode와 서버 상태 소유권
- 상태: Accepted
- 결정일: 2026-07-26
- 적용 브랜치: `feature-frontend-routing-release-recovery-runtime`
## 배경
기존 라우터는 `BrowserRouter`와 수동 JSX route 목록을 사용했다. 직렬화 가능한
route registry에 params/search schema, loading/error surface, access, title,
navigation과 chunk ID가 있었지만 실행 route tree와 독립적이어서 선언과 행동이
어긋날 수 있었다.
이 저장소는 client-only SPA이며 서버 상태는 application input과 TanStack Query가
소유한다. Framework Mode의 loader/action 중심 데이터 소유권이나 SSR을 도입하지
않으면서 route object, 오류 경계와 navigation lifecycle은 중앙에서 조립할
필요가 있다.
## 결정
1. 고정된 React Router `7.18.1``createBrowserRouter``RouterProvider`
사용하는 Data Mode를 기본값으로 채택한다.
2. 직렬화 가능한 route contract와 React component/codec runtime map을 분리한다.
3. 모든 executable route object와 navigation은 registry에서 생성한다. JSX에서
route 목록을 다시 열거하지 않는다.
4. params/search는 route 경계의 Zod codec으로 parse하고 같은 codec으로 canonical
URL을 생성한다.
5. loader/action은 같은 서버 데이터를 직접 다시 요청하지 않는다. 필요하면
application input 또는 query adapter 한 경로를 호출한다.
6. 서버 상태, retry, cache와 mutation lifecycle은 application input과 TanStack
Query가 계속 소유한다.
7. lazy chunk rejection만 release recovery input으로 보내며 일반 render error는
route/feature boundary가 소유한다.
8. Framework Mode, SSR, static generation과 router version upgrade는 별도
dependency/architecture 브랜치에서 결정한다.
## 검증
- route contract/runtime map의 누락과 orphan은 TypeScript negative fixture와
registry gate가 모두 거절한다.
- duplicate ID/path, unknown codec/surface/chunk와 참조 불일치를 negative registry
fixture로 검증한다.
- params/search parse/build round-trip, canonical redirect, 최대 redirect hop,
access rejection, title/focus와 boundary reset을 unit/component test로 검증한다.
- Vite dynamic entry와 release route chunk map, runtime config JSON Schema를
build/release 검증기가 확인한다.
- chunk failure는 no-store manifest refetch 후 build/release 쌍마다 한 번만
reload하며 offline, malformed manifest와 storage 실패는 fail-closed한다.
## 결과와 rollback
Data Router는 navigation lifecycle의 조립 경계이며 서버 데이터 계층이 아니다.
이 구분을 지키면 React Router를 교체해도 application input과 output port는
유지된다.
rollback은 RP-04 merge를 되돌려 이전 수동 router와 generic route failure
surface로 복구한다. URL shape와 application API는 유지하고, 이미 배포된 asset
cache의 purge는 저장소 rollback 범위에 포함하지 않는다.
@@ -12,9 +12,9 @@
- 기본 번들에 포함할 역량과 필요할 때 설치할 확장 역량을 구분한다.
- 특정 벤더를 채택하더라도 제품 코드가 벤더 API에 직접 결합되지 않는지 확인한다.
검토 기준 브랜치는 `develop`, 기준 커밋은 `cb195f8`다. 이후 구현으로 경로나
세부 내용이 달라질 수 있으므로, 각 항목은 문서의 경로뿐 아니라 해당 테스트와
아키텍처 게이트로 계속 검증해야 한다.
최초 검토 기준 `develop` `cb195f8`며, RP-01~RP-04 구현 결과를 이 문서에
누적 반영했다. 이후 구현으로 경로나 세부 내용이 달라질 수 있으므로, 각 항목은
문서의 경로뿐 아니라 해당 테스트와 아키텍처 게이트로 계속 검증해야 한다.
## 2. 결론
@@ -33,13 +33,14 @@
특히 다음은 선행 해결이 필요하다.
RP-01~RP-03에서 TypeScript 도구 안전망, application runtime 주입,
query/mutation inbound adapter HTTP 실행 계약은 구현됐다. 현재 선행 해결
RP-01~RP-04에서 TypeScript 도구 안전망, application runtime 주입,
query/mutation inbound adapter, HTTP 실행 계약과 executable route/release
recovery 계약은 구현됐다. 현재 선행 해결
대상은 다음과 같다.
1. 선언과 실행이 일치하는 typed route 계약
2. 전체를 제거할 수 있는 실제 reference feature
3. 폼, 페이지 템플릿, 확장된 디자인 시스템과 컴포넌트 워크벤치
1. 전체를 제거할 수 있는 실제 reference feature
2. 폼, 페이지 템플릿, 확장된 디자인 시스템과 컴포넌트 워크벤치
3. 국제화, diagnostics, optional adapter recipe와 심화 품질 게이트
따라서 현재 상태를 “프론트 공통부가 모두 구현됐다”고 표현하면 범위가 과장된다.
더 정확한 표현은 다음과 같다.
@@ -72,7 +73,7 @@ query/mutation inbound adapter와 HTTP 실행 계약은 구현됐다. 현재 선
| 서버 상태 | 부분 준비 | 제한된 query/mutation bridge와 lifecycle test | RP-05 reference route에서 실제 feature 연결 |
| 클라이언트 상태 | 부분 준비 | local state, theme context, session external store | 상태 소유권 표와 typed external-store 예제 |
| 범용 global store | 프로젝트 선택 | 별도 라이브러리 없음 | 필요 조건에 따라 Zustand/Redux Toolkit/state machine 선택 |
| 라우팅 | 부분 준비 | lazy route, access hint, registry 존재 | typed runtime map, codec, recovery, metadata 집행 |
| 라우팅 | 준비 | Data Router, typed runtime map, codec, metadata consumer, bounded chunk recovery | reference feature route와 release E2E로 사용 범위 확장 |
| 앱 셸·반응형 | 부분 준비 | header/sidebar/content/theme 구현 | 접근 가능한 mobile drawer와 focus 복원 |
| 페이지 템플릿 | 미제공 | 각 페이지가 직접 레이아웃 조립 | list/detail/form/status 등 슬롯 기반 템플릿 |
| 디자인 토큰 | 부분 준비 | semantic color/theme 토큰 존재 | typography, spacing, motion, layer 등 3단계 토큰 |
@@ -156,14 +157,16 @@ client를 거대한 범용 함수로 계속 확장하지 말고 transport, reque
timeout, retry, decoder, mapper 책임을 분리해야 한다. application에는 범용 HTTP
메서드보다 feature가 요구하는 gateway interface를 노출한다.
#### route registry 실행 계약이 아니다
#### RP-04에서 route registry 실행 계약으로 전환
route registry에는 `paramsSchema`, `searchSchema`, `loadingSurface`,
`errorSurface`, `chunkId`가 있지만 실제 router tree, lazy module, navigation
목록은 별도로 작성된다. 여러 필드는 선언만 되고 런타임에 사용되지 않는다.
chunk recovery use case와 redirect loop guard도 실제 route flow에 연결되지 않는다.
`src/contracts/routes.js`와 직렬화 가능한
`src/contracts/route-runtime-contract.js`를 기준으로
`src/presentation/routes/app-router.tsx`가 Data Router route object와
navigation을 생성한다. `route-runtime.tsx`는 lazy component의 실행 map만
소유하며 contract/runtime 누락과 orphan은 TypeScript negative fixture와 registry
gate가 모두 거절한다.
목표 상태:
현재 보장:
- serializable contract와 executable runtime map을 분리한다.
- `satisfies Record<RouteId, RouteRuntime>`로 양방향 완전성을 검사한다.
@@ -171,8 +174,10 @@ chunk recovery use case와 redirect loop guard도 실제 route flow에 연결되
사용한다.
- loading/error/chunk/access/title/navigation metadata를 실제 route object에
연결한다.
- route change 시 boundary reset, focus, scroll, navigation cancellation을
검증한다.
- route change 시 boundary reset, title, focus scroll을 검증한다.
- Vite manifest의 실제 dynamic entry와 route chunk ID를 release manifest에
연결하고, no-store manifest 재조회와 build/release 쌍별 1회 reload를
production application input까지 연결한다.
#### reference feature가 완전히 제거되지 않는다
@@ -204,9 +209,11 @@ mutation-pending
mutation-conflict
```
chunk recovery와 release coherence도 policy 함수가 존재하는 것으로 완료되지
않는다. 실제 lazy import failure가 manifest 재확인, build 비교, 단 한 번의 guarded
reload, 반복 실패 지원 표면까지 이어지고 E2E로 검증되어야 한다.
RP-04에서 lazy import failure는 `ChunkRecoveryBoundary` → application recovery
input → `ReleaseInfoPort.refresh()`의 no-store manifest 조회 → build/release 쌍
guard → browser navigation adapter의 1회 reload로 연결됐다. 일반 render
failure는 이 경로에서 제외되고, 반복 실패·offline·malformed manifest·storage
실패는 지원 표면으로 fail-closed된다.
#### telemetry, registry, 공급망 gate의 실행 깊이가 부족하다
@@ -378,7 +385,8 @@ tree-shakable SVG icon source로 적절하지만 select, dialog, menu, focus man
- TS source와 test 전체 typecheck
- 실제 composition root부터 page까지의 통합
- query/mutation controller와 optimistic rollback
- route registry/runtime map 정합성
- route registry/runtime map 정합성은 RP-04에서 unit, component, negative
registry/type fixture와 built artifact 검증으로 구현됨
- runtime timeout/retry와 path/query/parsed body
- shared MSW scenario catalog
- isolated component stories와 interaction test
@@ -154,11 +154,13 @@ RP-03 구현으로 HTTP와 server-state 경계도 다음처럼 연결됐다.
- HTTP가 자동 network retry를 소유하고 query/mutation adapter의 vendor retry는
비활성화한다.
RP-04에서 첫 번째 실행 불일치는 닫혔다. route registry와 runtime map은
Data Router tree, codec, surface, title, navigation, chunk/release recovery의
단일 조립 입력이며 registry/type/build 검증이 누락과 orphan을 거절한다.
후속 브랜치에서 닫아야 할 실행 불일치는 다음과 같다.
1. route registry의 `paramsSchema`, `searchSchema`, `loadingSurface`,
`errorSurface`, `chunkId` 일부는 실행 route와 연결되지 않았다.
2. 제거 테스트는 `src/sample/contract-fixture`만 제거하며, sample API
1. 제거 테스트는 `src/sample/contract-fixture`만 제거하며, sample API
operation, Zod schema, mapper, domain model과 query key는 다른 경로에
남는다.
@@ -1212,9 +1214,9 @@ contract와 실패 분기를 우선한다.
### 26.4 Routing과 상태
- [ ] route registry와 실행 route tree가 동일 source에서 생성된다.
- [ ] params/search schema가 실제 navigation에서 실행된다.
- [ ] loading/error/chunk/access metadata가 실행 behavior와 연결된다.
- [x] route registry와 실행 route tree가 동일 source에서 생성된다.
- [x] params/search schema가 실제 navigation에서 실행된다.
- [x] loading/error/chunk/access metadata가 실행 behavior와 연결된다.
- [ ] local, URL, server, session, persisted state가 분류 규칙을 따른다.
- [ ] server state를 별도 global store에 중복 보관하지 않는다.
+18 -20
View File
@@ -14,24 +14,22 @@
## 2. 현재 상태와 문제
현재 구현에는 다음 장점이 있다.
RP-04 이후 현재 구현에는 다음 장점이 있다.
- route registry가 path와 access policy를 소유한다.
- route component를 lazy import한다.
- contract에서 Data Router route object와 navigation을 생성한다.
- runtime map이 route component를 lazy import하고 codec을 연결한다.
- 앱 셸과 보호 route, not-found surface가 있다.
- route heading focus와 비동기/render error boundary가 있다.
- redirect loop와 chunk recovery에 대한 policy 함수가 일부 존재한다.
- redirect loop와 chunk recovery가 bounded production call graph에 연결돼 있다.
하지만 `src/contracts/routes.js`의 metadata와
`src/presentation/routes/app-router.jsx`의 executable route tree가 별도 수동 목록이다.
그 결과 다음 필드는 선언돼도 실제 행동을 보장하지 않는다.
- params/search schema
- loading/error surface
- chunk ID
- route title/navigation label
- redirect loop guard
- chunk recovery policy
`src/contracts/routes.js`, `src/contracts/route-runtime-contract.js`,
`src/presentation/routes/route-runtime.tsx`의 완전성은 TypeScript와 registry
negative fixture가 함께 검사한다. params/search codec, loading/error surface,
access, title, navigation, chunk ID는
`src/presentation/routes/app-router.tsx`에서 모두 소비된다. built Vite
manifest의 dynamic entry는 release manifest route chunk map과 검증되며,
`ChunkRecoveryBoundary`는 일반 render error와 chunk rejection을 분리한다.
페이지도 공통 `PageHeader` 외에는 각자 section과 class를 직접 조립한다. 목록,
상세, 편집, 오류 페이지의 반복되는 접근성·반응형·상태 표면을 기능 팀이 다시
@@ -50,15 +48,14 @@ selector를 `7.18.1`로 맞춰 확인한다.
| mode | 선택 조건 | 이 저장소에서의 판단 |
| --- | --- | --- |
| Declarative | React composition과 외부 data layer가 route data를 소유 | 현재 구현이 사용 중인 기준선 |
| Data | route object, blocker, scroll restoration, pending/navigation state가 필요 | 목표 skeleton의 navigation lifecycle에 적합 |
| Declarative | React composition과 외부 data layer가 route data를 소유 | RP-04 이전 기준선 |
| Data | route object, blocker, scroll restoration, pending/navigation state가 필요 | VD-03으로 채택하고 RP-04에서 구현 |
| Framework | route module, type-safe href, code splitting, SSR/static 전략을 framework가 소유 | client-only skeleton 기본값으로는 범위가 큼 |
목표 결정:
채택한 결정:
- client-only SPA와 TanStack Query/application use case를 유지한다.
- 현재 `BrowserRouter` 기반 Declarative Mode에서 `createBrowserRouter`
`RouterProvider` 기반 Data Mode로 이동한다.
- `createBrowserRouter``RouterProvider` 기반 Data Mode를 사용한다.
- Data Mode를 선택하는 이유는 route object, navigation blocker, scroll
restoration, route error 경계를 일관되게 소유하기 위해서다. loader/action으로
서버 상태를 다시 소유하기 위해서가 아니다.
@@ -68,8 +65,9 @@ selector를 `7.18.1`로 맞춰 확인한다.
- SSR/static generation을 선택하기 전에는 Framework Mode를 기본값으로 만들지
않는다.
전환 브랜치 전까지 현재 Declarative router에 새 custom scroll/blocker
implementation을 추가하지 않는다. 전환할 수 없는 프로젝트만 별도 ADR과
결정 근거와 rollback 경계는
`docs/architecture/decisions/VD-03-react-router-data-mode.md`에 고정한다.
Data Mode를 사용할 수 없는 프로젝트만 별도 ADR과
`NavigationLifecycleAdapter`를 구현한다.
## 4. route 계약과 runtime map
@@ -370,10 +370,9 @@ QueryClientProvider
-> AppShell
```
04 routing branch에서 Data Mode로 전환할 때 `BrowserRouter`
`RouterProvider`로 바꾸고 테스트 fixture도 같은 composition factory에서
생성한다. 문서에 적힌 provider 순서를 테스트 전용 shell로 재현하지 말고
production composition 함수를 호출한다.
RP-04에서 `RouterProvider` 기반 Data Mode로 전환했다. router component test와
runtime composition test는 production `AppRouter` composition 함수를 사용하며,
문서에 적힌 provider 순서를 테스트 전용 shell로 재현하지 않는다.
### 6.3 Boot E2E
@@ -1273,7 +1272,8 @@ CI registry에 추가한다.
3. MSW handlers/scenario/factory를 중앙 catalog로 이동한다.
4. query/mutation presentation adapter와 integration harness를 만든다.
5. form foundation과 form/controller test matrix를 만든다.
6. route registry/runtime map contract와 built-dist E2E를 추가한다.
6. route registry/runtime map contract와 built-dist artifact 검증을 유지하고,
release server를 사용하는 built-dist E2E까지 확장한다.
7. Storybook build, interaction, a11y gate를 추가한다.
8. pinned Chromium visual baseline을 추가한다.
9. critical flow의 3-engine release profile을 분리한다.