feat: execute route and release recovery contracts
This commit is contained in:
@@ -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에 중복 보관하지 않는다.
|
||||
|
||||
|
||||
@@ -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을 분리한다.
|
||||
|
||||
Reference in New Issue
Block a user