refactor: 프론트 템플릿 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 15:16:58 +09:00
parent c10a709f2c
commit 5cc41467ae
80 changed files with 7227 additions and 4672 deletions
@@ -0,0 +1,127 @@
# Capability consumer experience baseline
Correctness inside a reusable capability is not sufficient. The platform is
also evaluated by how much of that correctness a normal feature developer must
understand.
These measurements are baselines, not score targets. A lower line count is not
automatically better if it hides business semantics or creates a universal
repository abstraction.
## Scenario A — reference REST feature
The existing reference feature is the executable REST consumer.
It covers list/detail/create behavior, mapping, typed operation inputs,
application ports and an optimistic mutation path.
Current source size:
| Feature-owned area | LOC |
| --- | ---: |
| `application/reference-feature-api.ts` | 91 |
| `contracts/reference-mapper.ts` | 109 |
| `adapters/reference-http-gateway.ts` | 65 |
| `adapters/create-reference-feature-input.ts` | 41 |
| Total measured boundary/application source | 306 |
The important boundary metric is not the raw total. It is what those files need
to know about the platform.
Current result:
- HTTP platform imports in the feature adapter layer: 2 files,
- canonical import path used by both:
`src/adapters/http/index.ts`,
- direct imports of `http-execution-v3.ts`, retry scheduler, response reader,
auth admission or effect-certainty internals: 0,
- feature gateway owns transport failure projection: 0,
- feature gateway chooses operation ID, route ID, exact input/value type and
mapper: yes,
- central installed files own feature-specific adapter/runtime wiring: no;
feature-owned contributions are aggregated centrally.
The reusable HTTP capability now owns execution-outcome normalization through
`createFeatureHttpBinding`. A feature may still implement a custom outbound
adapter behind its application port when the reusable contract does not match
its business requirement.
## Scenario B — IndexedDB local draft
An executable consumer probe lives at:
`tests/contract/consumer-experience/indexeddb-local-draft.test.ts`
with the feature-owned fixture:
`tests/contract/consumer-experience/fixtures/local-draft-feature.ts`
The fixture models a small local-draft feature with save/find/remove and
optimistic revision checking.
Measured result:
| Metric | Result |
| --- | ---: |
| feature-owned fixture LOC | 97 |
| platform import statements | 1 |
| native IndexedDB API references | 0 |
| `src/adapters/storage/indexeddb/**` imports | 0 |
| runtime-internal IndexedDB types imported by feature | 0 |
The feature depends only on the public application boundary:
`src/application/ports/browser-file-storage/index.ts`
and specifically `IndexedDbRepositoryPort<LocalDraft, never>`.
The contract test rejects feature source that reaches for
`globalThis.indexedDB`, `IDBFactory`, `IDBDatabase`, `IDBTransaction`,
`IDBObjectStore` or the concrete IndexedDB adapter directory.
## What this does and does not prove
The probe confirms that **feature business code does not need native IndexedDB
knowledge** once an `IndexedDbRepositoryPort` has been composed.
It does not prove that composition of `createIndexedDbRuntime` is cheap.
That constructor still owns substantial infrastructure policy:
- dataset scope and storage policy,
- physical store governance,
- retention/idempotency stores,
- schema migrations,
- codec and query policy,
- lifecycle authority,
- durability, scheduling and observation.
That complexity belongs at the composition/platform boundary, not in the
feature. A new `create...Repository<T>` convenience factory should be added
only when a second real product consumer demonstrates which subset is stable
enough to become a reusable profile. Creating one now would guess at policy and
risk producing a universal storage abstraction.
## Consumer-quality review checklist
For each new product feature, record:
- feature-owned adapter LOC,
- platform glue LOC,
- files changed,
- central catalog edits,
- direct imports from capability-internal modules,
- native browser/network API references,
- duplicated failure/retry/lifecycle policy.
A healthy feature path should look like:
1. domain type and invariant,
2. use case,
3. business port,
4. feature-owned mapper/codec and policy,
5. capability-specific binding,
6. presentation controller/page.
The feature should not need the retry scheduler, abort ownership,
effect-certainty machinery, IndexedDB transaction lifecycle, OPFS journal,
reconnect coordinator or provider process model.
+110
View File
@@ -0,0 +1,110 @@
# Contract ownership
`src/contracts` is not a default destination for every shared-looking type.
A contract belongs there only when the change authority is genuinely shared
across layers, capabilities, build tooling, or runtime composition.
## Decision rule
For every proposed contract, ask:
1. Which requirement can cause this type or policy to change?
2. Is there one clear capability or feature owner?
3. Does another runtime/tooling boundary consume the same semantic contract?
4. Is the contract a wire/artifact authority shared by browser runtime and
build/release tooling?
The placement rule is:
- one feature owner -> keep it under that feature,
- one reusable capability owner -> keep it under that adapter/capability and
export it through the capability public entry point,
- multiple independent capability/layer owners -> `src/contracts`,
- browser/build/release wire or artifact authority -> `src/contracts` even
when the browser source graph alone looks small.
Consumer count alone is not sufficient. Scripts, generated artifacts and
release gates are semantic consumers too.
## Audit result
The September 2026 architecture review triggered an import-graph audit of all
39 contract files.
### Capability-owned contract moved
`cursor-pagination.ts` had one production owner:
`src/adapters/query-cache/cursor-pagination-runtime.ts`.
It moved to:
`src/adapters/query-cache/cursor-pagination-contract.ts`
and is exported through:
`src/adapters/query-cache/index.ts`
Tests use that public capability entry point. Pagination vocabulary no longer
occupies the global contract bucket merely because it is reusable inside one
adapter.
### Contracts intentionally kept global
The following examples have multiple semantic owners and remain global:
- `errors.ts` — application, presentation and several adapters,
- `result.ts` — common success/failure carrier below application,
- `boundary-mapper.ts` — HTTP, browser RPC, realtime and feature registries,
- `mutation-intent.ts` — application, presentation, HTTP, platform and
bootstrap,
- `exact-snapshot.ts` — HTTP, query-cache and browser-transfer,
- `rest-profiles.ts` — auth, HTTP, bootstrap and feature contract validation,
- `query-invalidation.ts` / `query-keys.ts` — query-cache, presentation,
bootstrap and features,
- `cache-invalidation.ts` — cross-context wire protocol plus query
invalidation policy,
- `storage-keys.ts` — browser storage and cross-context invalidation,
- `telemetry.ts` / `diagnostics.ts` — runtime adapters, application and
bootstrap.
Some contracts appear to have few browser-source consumers but are still shared
authorities:
- `env.ts` is consumed by bootstrap, runtime-schema/security tests and
registry governance,
- `deployment-admission.ts` is shared by runtime-config generation and release
admission,
- `release-tokens.ts` is shared by runtime coherence tooling and tests,
- `service-worker-static-manifest.ts` is a runtime-neutral canonical format
shared by build generation, validation and service-worker evidence.
Moving those based only on `src/**` import counts would split one semantic
authority across processes.
## Feature contracts
Feature-specific contracts stay inside the vertical slice:
`features/<feature>/contracts`
The reference feature owns routes, schemas, mapper definitions, message
catalogs and contribution identities. Central installed files aggregate those
contributions; they do not own their semantics.
The message catalog is a deliberate special case: the compiled catalog remains
total even when a feature is build-time disabled so the typed message lookup
does not become partial. The central message file therefore aggregates compiled
message keys rather than treating runtime installation as message ownership.
## Review rule for future additions
A new file under `src/contracts` should be rejected during review when all of
the following are true:
- one feature or one capability is the only semantic owner,
- no build/release/runtime wire authority needs the same definition,
- moving the definition to that owner does not create an inward dependency
violation.
Do not move a contract merely to reduce the number 39. The goal is explicit
ownership, not a smaller directory.
@@ -0,0 +1,219 @@
# Frontend Application Foundation
This repository is not treated as a minimal React project template and it is not
an independent general-purpose SDK. Its architectural role is:
> Frontend Application Foundation = Starter / Composition Skeleton + Reusable Capability Platform
The starter side owns bootstrap, routing, providers, project conventions and a
removable reference feature. The capability side owns reusable technical
problems such as HTTP execution, Server State, authentication boundaries,
IndexedDB/OPFS, Cache Storage, realtime, browser RPC, transfer and diagnostics.
The cost of a sophisticated capability is acceptable only when product features
do not have to understand that internal sophistication.
## Hybrid architecture
Platform code is horizontal:
- `application`: generic application inputs/policies/ports
- `contracts`: genuinely cross-capability shared vocabulary and registries
- `adapters`: reusable capability runtimes
- `presentation`: generic UI/routing/query integration
- `bootstrap`: concrete composition
Product business code is vertical:
- `features/<feature>/domain`
- `features/<feature>/application`
- `features/<feature>/contracts`
- `features/<feature>/adapters`
- `features/<feature>/presentation`
The dependency model is:
Domain / Use case
|
| owns
v
Business port
^
| implements / binds
|
Feature-owned adapter binding
|
| generic type + mapper/codec + policy
v
Reusable capability runtime
|
v
Browser / network / native API
A use case never imports a platform adapter. Generic binding happens in the
feature adapter/composition seam.
## Capability-specific typed bindings
Do not introduce one universal `Repository<TKey, TValue>` abstraction for HTTP,
storage, realtime and transfer. Their lifecycle and failure semantics differ.
A reusable capability boundary is composed from:
- generic input/output types,
- feature-owned mapper or codec,
- feature-selected policy,
- one capability-specific runtime.
The HTTP reference path is the first concrete example.
`src/adapters/http/feature-http-binding.ts` owns transport/outcome
normalization. The reference feature contributes only:
- operation ID,
- route ID,
- exact request input type,
- exact success value type,
- wire-to-domain mapper.
The feature gateway therefore does not reimplement timeout, cancellation,
transport failure, authentication failure or contract-violation projection.
Storage, realtime and transfer may gain their own typed binders only after
actual feature repetition demonstrates the need. They must not be forced
through the HTTP abstraction.
## Custom adapter escape hatch
A product feature uses a reusable capability when the capability preserves the
business requirement.
If a platform contract would require changing or weakening the business model,
the feature implements a custom outbound adapter behind the same application
port. The architecture boundary remains stable; platform reuse is optional.
## Feature installation
A feature owns its contract, runtime and adapter contributions.
Central installed catalogs are aggregation points only:
- `installed-product-manifest.ts`: which product features are compiled/selected
- `installed-feature-contracts.ts`: contract aggregation
- `installed-feature-runtimes.tsx`: runtime contribution aggregation
- `installed-feature-adapters.ts`: application-input contribution aggregation
Feature-specific composition belongs under the feature itself. Central
catalogs must not grow feature-specific branching logic.
## Presentation consumer surface
`ApplicationProvider` remains the composition root for presentation, but new
consumers should not navigate a root `ApplicationApi` service locator.
Use the narrow hooks in
`src/presentation/providers/application-provider.tsx`:
- `useApplicationSession`
- `useApplicationPreferences`
- `useApplicationDiagnostics`
- `useApplicationRuntime`
- `useApplicationRecovery`
- `useApplicationFeature`
`useApplication` exists only as a deprecated compatibility escape hatch.
## Canonical imports
New feature code should use capability public entry points rather than deep
runtime modules. For HTTP the canonical path is
`src/adapters/http/index.ts`.
Compatibility re-exports may exist during a migration window, but they must be
marked as compatibility/deprecated paths and should not expand into an
unbounded public barrel.
## Policy ownership
Duplication is judged by ownership, not by syntax percentage.
Small local validators can remain duplicated when locality improves auditing.
Business or concurrency policy must have one owner. For example the reference
create mutation keeps definition ID, idempotency requirement, duplicate policy
and invalidation policy in one feature-owned definition and binds only
`scope` and `execute` per usage site.
## Runtime decomposition rule
Large runtime files are not split by line count.
Extract a boundary when it has its own state machine, lifecycle owner, failure
model or compensation/recovery responsibility. Candidate seams include:
- connection/open/upgrade lifecycle,
- transaction ownership,
- migration state machine,
- reconnect/backoff and heartbeat,
- subscription ownership,
- retry/deadline/cancellation ownership,
- settlement/reconciliation/cleanup.
A cohesive 2,000-line state machine can remain together. A 300-line file with
multiple lifecycle owners is a better extraction candidate.
### Current runtime boundary audit
The current large-runtime inventory was reviewed using that rule.
- IndexedDB remains large, but connection, transaction, migration, maintenance
and failure translation already have separate owners/modules.
- resumable upload already separates runtime policy, checkpoint persistence,
HTTP control-plane transport, part execution, cancellation and mutation
locking.
- realtime already separates reconnect policy/coordinator, event codec/consumer
and stream coordination.
- OPFS is separated into browser runtime, journal, byte-store, policy and worker
protocol/runtime responsibilities.
- HTTP V3 still owned retry eligibility/backoff inside the execution state
machine, so that responsibility moved to
`src/adapters/http/http-retry-lifecycle.ts`.
No other runtime is split merely because of its line count.
## Consumer quality metrics
Before adding another abstraction, implement or model multiple real feature
uses and measure:
- feature-owned adapter LOC,
- repeated platform glue,
- number of platform-internal types exposed to the feature,
- central catalog edits,
- files changed for one normal query/command,
- whether native browser/network APIs leak into the feature.
The target feature-development path is:
1. domain type and invariant,
2. use case,
3. port,
4. transport/storage schema plus mapper/codec,
5. capability binding,
6. presentation controller/page.
A product feature should not need to know the retry scheduler, abort ownership,
effect-certainty machinery, transaction leases, reconnect coordinator, OPFS
journal or provider lifecycle.
The executable REST and IndexedDB consumer baselines are recorded in
[`capability-consumer-experience.md`](./capability-consumer-experience.md).
Contract placement and the global-vs-owner-local audit are recorded in
[`contract-ownership.md`](./contract-ownership.md).
## Verification paths
Product-development verification, capability verification and release assurance
are intentionally separate. See
[`docs/testing/taxonomy.md`](../testing/taxonomy.md).
Host-level CI-runner tests belong to `tests/system`, not `tests/unit`.
Reusable capability consumer contracts belong to `tests/contract`.
File diff suppressed because it is too large Load Diff
@@ -90,11 +90,19 @@ production Playwright profile은 source fixture가 아니라 `build` + `preview`
#### TanStack Query의 React integration test 기반
`tests/component/application-query.test.tsx`는 production query inbound
adapter의 query/mutation lifecycle을 검증한다. cancellation, initial terminal
failure, background stale-failure latch와 retry 복구, duplicate submit,
optimistic commit/rollback, conflict 해제와 namespace invalidation이 실제
QueryClient 위에서 실행된다. HTTP 자동 retry가 소유자이므로 이 adapter의
production query inbound adapter의 React integration은 behavior owner별
component suite로 분리되어 있다.
- `application-query-bridge.test.tsx`: initial/background state, cancellation
- `application-query-scope-fence.test.tsx`: scope commit fence와 result budget
- `application-mutation-scope-fence.test.tsx`: mutation scope fence
- `application-query.test.tsx`: unknown-effect reconciliation
- `application-mutation-admission.test.tsx`: intent/duplicate admission
- `application-mutation-optimistic-cache.test.tsx`: optimistic cache commit/rollback
- `application-query-fixture.tsx`: 공통 QueryClient/scope fixture
각 실패 파일명이 깨진 behavior contract를 직접 드러내며, 동일한 52개 계약을
실제 QueryClient 위에서 검증한다. HTTP 자동 retry가 소유자이므로 이 adapter의
query/mutation vendor retry는 꺼져 있다.
#### Form과 route 위험
+65 -14
View File
@@ -3,14 +3,68 @@
Each gate is blocking in its declared scope. Failures are not downgraded with
`continue-on-error` or warning-only scripts.
| Level | Command | Evidence |
| --- | --- | --- |
| runtime schema | `pnpm test:runtime-schema` | `artifacts/tests/runtime-schema.xml` |
| unit | `pnpm test:unit` | `artifacts/tests/unit.xml` |
| component | `pnpm test:component` | `artifacts/tests/component.xml` |
| integration | `pnpm test:integration` | `artifacts/tests/integration.xml` |
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
## Executable levels
| Level | Command | Ownership / prerequisite | Evidence |
| --- | --- | --- | --- |
| runtime schema | `pnpm test:runtime-schema` | pure runtime schema contracts | `artifacts/tests/runtime-schema.xml` |
| unit | `pnpm test:unit` | Node-only domain/application/pure policy/runtime units; no systemd/bwrap/cgroup prerequisite | `artifacts/tests/unit.xml` |
| capability contract | `pnpm test:contract` | reusable capability consumer contracts | `artifacts/tests/contract.xml` |
| component | `pnpm test:component` | React/hook/UI behavior | `artifacts/tests/component.xml` |
| integration | `pnpm test:integration` | HTTP/MSW, IndexedDB and composed browser-runtime boundaries | `artifacts/tests/integration.xml` |
| system / CI runner | `pnpm test:system` | compatible Linux host with systemd, bubblewrap, cgroup v2 and CI-provider process controls | `artifacts/tests/system.xml` |
| end-to-end | `pnpm test:e2e` | pinned browser engines | `artifacts/tests/e2e/` |
| accessibility | `pnpm test:a11y` | pinned browser engines | `artifacts/tests/a11y.json` |
`test:all` is the normal product-development loop. It intentionally includes
runtime-schema, unit, capability-contract, component, integration, reference
feature and recipe suites, but does not include `test:system`. CI-runner and
supply-chain assurance has different host prerequisites and is invoked
explicitly in the assurance path.
## Deterministic test process
All Vitest package scripts launch through `scripts/run-vitest.ts`.
That runner:
1. rejects Node versions outside the repository-supported
`>=24.11.0 <25.0.0` range before the suite starts,
2. owns `NODE_ENV=test` rather than trusting the parent shell,
3. removes host-specific `npm_config_userconfig`, `npm_config_prefix` and
`npm_config_globalconfig` values before Vitest starts.
`vitest.config.ts` also fixes `NODE_ENV=test` so a direct Vitest invocation
cannot accidentally select React's production behavior.
The system suite additionally runs
`scripts/check-system-test-prerequisites.ts` and fails with one prerequisite
report when the CI-runner host does not provide its required Linux facilities.
## Development paths
Product feature:
- focused feature/unit/component test
- capability contract test when a reusable boundary changes
- type/lint/architecture
- `test:all`
Reusable capability:
- focused unit tests
- capability contract tests
- integration tests
- type/lint/architecture
CI / release assurance:
- `test:system`
- supply-chain / promotion / release gates
A host-level process/sandbox test must not be placed in `tests/unit` merely
because it uses Vitest. The classification follows the system boundary and
prerequisites, not the test framework.
End-to-end and automated accessibility scenarios run on the pinned Chromium,
Firefox, and WebKit engines. The responsive contract explicitly exercises
@@ -30,9 +84,6 @@ Promotion is an AND graph:
3. release gates plus rollback/runbook drills
4. production promotion plus eligible field Web Vitals evidence
This file describes the currently registered taxonomy. The
[frontend platform testing strategy](./frontend-platform-testing-strategy.md)
documents the target additions: test TypeScript projects, real bootstrap
composition tests, shared MSW scenarios, query/mutation/form/router coverage,
Storybook interaction and accessibility checks, visual regression, and a
built-output Playwright profile.
The [frontend platform testing strategy](./frontend-platform-testing-strategy.md)
contains the broader testing design. This file is the executable taxonomy for
where a test belongs and which environment is allowed to run it.