1357 lines
50 KiB
Markdown
1357 lines
50 KiB
Markdown
# 프론트엔드 포트·어댑터와 경계 설계
|
|
|
|
> **정본 안내 (non-authoritative for runtime capability decisions)**
|
|
>
|
|
> Runtime Config/boot, Fetch HTTP client, Router, Query/Mutation, realtime 공통 경계, Web Worker,
|
|
> Service Worker, offline command와 Background Sync의 구현 결정은
|
|
> [프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계](./2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md)가 정본이다.
|
|
> 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.
|
|
|
|
|
|
## 1. 문서 목적
|
|
|
|
이 문서는 이 저장소에서 도메인 기능을 추가할 때 사용할 경계와 확장
|
|
절차를 정의한다. 다음 질문에 대한 단일 기준 문서다.
|
|
|
|
- 프론트엔드에서 inbound와 outbound는 무엇인가?
|
|
- `presentation`은 왜 inbound adapter인가?
|
|
- application이 노출해야 할 API와 외부 기술 포트는 어떻게 다른가?
|
|
- React, Router, TanStack Query, HTTP, 인증, 저장소, 로깅을 어디에
|
|
배치하는가?
|
|
- 새 기술 adapter나 새 feature를 어떤 순서로 추가하는가?
|
|
- 예제 feature가 제품 코드에 남지 않았음을 어떻게 증명하는가?
|
|
|
|
포트의 방향은 네트워크 패킷의 방향이 아니라 **application을 기준으로
|
|
누가 누구를 호출하는지**로 결정한다.
|
|
|
|
```text
|
|
사용자·브라우저 이벤트
|
|
-> inbound adapter
|
|
-> input port
|
|
-> application use case
|
|
-> output port
|
|
-> outbound adapter
|
|
-> 외부 시스템 또는 브라우저 기능
|
|
```
|
|
|
|
화살표는 런타임 호출 방향이다. 소스 의존성은 가능한 한 안쪽을 향한다.
|
|
구체 기술의 선택과 조립은 `bootstrap`에서만 수행한다.
|
|
|
|
## 2. 핵심 용어
|
|
|
|
### 2.1 Input port
|
|
|
|
외부가 application에 요청할 수 있는 작업의 계약이다. 사용자의 의도를
|
|
나타내며 기술 이름 대신 업무 동사를 사용한다.
|
|
|
|
예:
|
|
|
|
- `ListReferenceItems`
|
|
- `CreateReferenceItem`
|
|
- `BeginSignIn`
|
|
- `ChangeColorScheme`
|
|
- `RecoverSession`
|
|
|
|
Input port는 React hook, HTTP request, router loader 자체가 아니다. React나
|
|
router가 없어도 테스트할 수 있는 함수 또는 인터페이스여야 한다.
|
|
|
|
### 2.2 Inbound adapter
|
|
|
|
사용자 입력이나 외부 이벤트를 input port 호출로 변환한다. 현재 저장소의
|
|
`src/presentation`은 이름에 `adapter`가 없지만 이 역할을 담당한다.
|
|
|
|
대표적인 inbound adapter:
|
|
|
|
- React page와 component
|
|
- React Router route, loader, action, guard
|
|
- form controller와 feature hook
|
|
- 키보드, pointer, browser lifecycle event handler
|
|
- WebSocket이나 Service Worker가 수신한 이벤트를 use case로 전달하는
|
|
event handler
|
|
|
|
### 2.3 Output port
|
|
|
|
application이 작업을 완료하기 위해 외부에 요구하는 capability 계약이다.
|
|
application이 소유하고 outbound adapter가 구현한다.
|
|
|
|
대표적인 output port:
|
|
|
|
- 도메인별 gateway 또는 repository
|
|
- session gateway
|
|
- clock, ID generator
|
|
- preference persistence
|
|
- logger, telemetry, error reporter
|
|
- feature flag reader
|
|
|
|
Output port에는 `fetch`, `localStorage`, 특정 SDK 객체처럼 기술 구현이
|
|
드러나지 않아야 한다.
|
|
|
|
### 2.4 Outbound adapter
|
|
|
|
output port를 실제 기술로 구현한다.
|
|
|
|
대표적인 outbound adapter:
|
|
|
|
- Fetch 기반 API gateway
|
|
- 외부 인증 SDK session gateway
|
|
- `localStorage`, `sessionStorage`, IndexedDB persistence
|
|
- telemetry 또는 error-reporting sink
|
|
- browser clock, random, crypto UUID
|
|
- feature flag SDK
|
|
|
|
### 2.5 Bootstrap
|
|
|
|
구체 구현을 선택하고 연결하는 composition root다. bootstrap만 다음
|
|
결정을 알고 있어야 한다.
|
|
|
|
- 어떤 HTTP transport를 사용할지
|
|
- 어떤 인증 owner를 연결할지
|
|
- 어떤 logger와 telemetry sink를 사용할지
|
|
- QueryClient 설정은 무엇인지
|
|
- 어떤 input port 구현을 `ApplicationProvider`에 공급할지
|
|
|
|
bootstrap은 page별 orchestration이나 업무 규칙을 소유하지 않는다.
|
|
|
|
## 3. 현재 저장소의 구조 해석
|
|
|
|
현재 구조는 다음과 같이 해석한다.
|
|
|
|
| 현재 경로 | 현재 역할 | 목표 역할 |
|
|
| --- | --- | --- |
|
|
| `src/domain` | 순수 model과 invariant | framework-neutral domain |
|
|
| `src/application` | port, policy, 일부 use case와 view model | input/output port와 use case |
|
|
| `src/presentation` | React UI, route, provider, 상태 화면 | inbound React adapter |
|
|
| `src/adapters` | HTTP, auth, storage, cache, telemetry 구현 | outbound adapter |
|
|
| `src/bootstrap` | runtime config와 구현 조립 | 유일한 composition root |
|
|
| `src/contracts` | 여러 계층의 registry가 혼재 | 소유 계층으로 분산 |
|
|
| `src/features/reference-feature` | 완전한 제거 가능 수직 예제 | installed contribution과 제거 gate 유지 |
|
|
|
|
현재 구조가 잘 제공하는 기반은 다음과 같다.
|
|
|
|
- bootstrap 이전 runtime config와 release 검증
|
|
- 인증 credential을 UI에 노출하지 않는 session seam
|
|
- bounded retry와 idempotency 정책
|
|
- HTTP envelope 및 payload runtime validation
|
|
- 저장소 key registry와 민감정보 저장 금지
|
|
- TanStack Query client 기본 정책
|
|
- semantic telemetry allowlist
|
|
- render boundary와 async state surface
|
|
- route registry와 lazy page
|
|
|
|
RP-02 구현으로 다음 경계는 실행 경로에 연결됐다.
|
|
|
|
- `src/bootstrap/composition-root.ts`가 만든 application을 production
|
|
`ApplicationProvider`가 실제 React tree에 주입한다.
|
|
- `createApplication`은 session, preference, diagnostics, runtime query와 typed
|
|
feature input registry만 반환하며 storage, telemetry, release output port를
|
|
숨긴다.
|
|
- bootstrap composition 결과는 raw output port를 반환하지 않고
|
|
application과 React infrastructure만 반환한다.
|
|
- presentation의 direct fetch/browser storage/concrete adapter/TanStack import와
|
|
application의 React/concrete adapter import는 negative fixture가 거절한다.
|
|
|
|
RP-03 구현으로 HTTP와 server-state 경계도 다음처럼 연결됐다.
|
|
|
|
- `src/presentation/adapters/query`만 TanStack Query import를 허용하며
|
|
application query/mutation을 cancellation, invalidation, deduplication,
|
|
optimistic rollback과 conflict 해제에 연결한다.
|
|
- HTTP request builder는 path escaping과 canonical search를 소유하고 Zod가
|
|
변환한 search/body를 실제 request에 사용한다.
|
|
- runtime timeout과 max retry attempts가 transport factory에 주입되며
|
|
validation, success, abort, timeout과 exhausted retry의 timer/listener
|
|
정리를 테스트한다.
|
|
- HTTP가 자동 network retry를 소유하고 query/mutation adapter의 vendor retry는
|
|
비활성화한다.
|
|
- reference HTTP operation map은 operation ID마다 route ID, request shape와
|
|
성공 model type을 결합한다. runtime schema와 mapper를 통과한 raw executor는
|
|
단일 binder에서만 typed executor로 승격되므로 gateway별 응답 cast가 없다.
|
|
|
|
위 항목은 reference REST vertical의 `COMPOSED` 증거다. auth owner final-request
|
|
invariant, auth unavailable fail-close, total deadline, bounded response decoder,
|
|
typed Schema/Mapper proof, complete pagination과 mutation concurrency 같은
|
|
production hardening delta는
|
|
[API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)에
|
|
별도 `DESIGNED_NOT_IMPLEMENTED`로 기록한다.
|
|
|
|
RP-04에서 route 실행 불일치는 닫혔다. route registry와 runtime map은
|
|
Data Router tree, codec, surface, title, navigation, chunk/release recovery의
|
|
단일 조립 입력이며 registry/type/build 검증이 누락과 orphan을 거절한다.
|
|
|
|
RP-05에서 두 번째 불일치도 닫혔다. feature별 domain/application/adapter/
|
|
contract/presentation은 `src/features/reference-feature`가 소유하고, generic
|
|
installed catalog만 bootstrap과 router에 노출된다. 제거 gate는 feature와 test를
|
|
삭제한 복제본에서 전체 P0 경로를 다시 실행한다.
|
|
|
|
generic application의 `ApplicationFeatureInputs`는 concrete feature를 import하지
|
|
않는 open interface다. 각 feature application API가 module augmentation으로
|
|
자신의 literal ID와 input shape를 기여하고, `features.get(id)`는 ID별 정확한
|
|
input type을 반환한다. 잘못된 ID/input과 설치 누락은 각각 compile-time negative
|
|
fixture와 runtime guard로 닫는다.
|
|
|
|
RP-06에서 inbound form/page 경계도 실행됐다. `src/presentation/forms`는 Zod
|
|
presentation schema, controlled field state, error focus, 422 allowlist,
|
|
pending/deduplication과 dirty navigation을 local facade로 감싼다.
|
|
`src/presentation/templates`는 slot과 landmark만 소유하고 application, HTTP,
|
|
query vendor import는 architecture gate가 거절한다. reference feature의
|
|
list/detail/create/status route가 각각 Collection/Detail/Form/Status template의
|
|
실제 consumer다.
|
|
|
|
RP-07에서 React inbound adapter 안의 UI 공급자 경계도 닫혔다.
|
|
`src/presentation/design-system/index.ts`는 token → primitive → pattern →
|
|
template public API이며 feature와 shell은 이 entry만 소비한다. Lucide는
|
|
`icons/vendors/lucide.tsx`에 격리된 inbound vendor facade이므로 application
|
|
port가 아니다. native Dialog/Drawer/Menu/Tabs의 focus·keyboard 상태도
|
|
presentation이 소유하고 use case나 outbound adapter로 올리지 않는다.
|
|
|
|
RP-08에서 i18n은 application output port가 아니라 React inbound adapter의
|
|
local facade로 확정됐다. domain/application은 locale이나 번역 문장을 알지 않고
|
|
timestamp, number, failure kind 같은 의미 값만 반환한다.
|
|
`src/presentation/i18n`이 typed message catalog, formatter, fallback,
|
|
`<html lang/dir>`과 pseudo/RTL smoke를 소유한다. backend raw `message`는
|
|
application failure registry를 우회해 렌더링할 수 없으며 `check:i18n` negative
|
|
fixture가 이 경계를 집행한다. 번역 vendor를 나중에 선택해도 이 facade 뒤의
|
|
adapter만 교체한다.
|
|
|
|
이 문서의 목표 구조는 기존 기반을 폐기하는 것이 아니라 이러한
|
|
불일치를 제거하는 것이다.
|
|
|
|
## 4. Inbound와 outbound 분류표
|
|
|
|
| 관심사 | 방향 | Port 소유자 | Adapter 예 | 기본 제공 여부 |
|
|
| --- | --- | --- | --- | --- |
|
|
| React page와 component | inbound | application input API 소비 | React | 필수 |
|
|
| Routing과 navigation | inbound | route input/controller | React Router | 필수 |
|
|
| Form submit과 validation 표시 | inbound | command input port 소비 | React form controller | 필수 |
|
|
| Server-state query hook | inbound bridge | application input API 소비 | TanStack Query hook | 필수 |
|
|
| validated external server event | inbound | event input port | SSE/WebSocket listener, Service Worker push handler | 선택 |
|
|
| 도메인 API 접근 | outbound | application | Fetch gateway | 필수 |
|
|
| 인증 session | outbound | application | 외부 auth owner/SDK | 필수 seam |
|
|
| Credential attachment | outbound | transport 또는 auth integration | auth request decorator | 필수 seam |
|
|
| Preference persistence | outbound | application | browser storage | 필수 |
|
|
| 시간·ID·random | outbound | application | browser platform | 필수 |
|
|
| Logging·telemetry | outbound | application | console/remote sink | 필수 |
|
|
| Offline persistence | outbound | application | IndexedDB | 선택 |
|
|
| Feature flag | outbound | application | flag SDK | 선택 |
|
|
| Locale/message/formatter | UI platform | React inbound 계약 | `Intl` + local catalog | 필수 seam, vendor 선택 |
|
|
| 외부 오류 수집 | outbound | application | error-reporting SDK | 선택 |
|
|
|
|
WebSocket은 한 단어로 항상 inbound 또는 outbound가 아니다. 연결을 열고
|
|
메시지를 전송하는 기능은 application이 요구하는 output capability일 수
|
|
있고, 수신된 메시지를 use case로 전달하는 handler는 inbound adapter다.
|
|
두 책임을 하나의 거대한 interface로 합치지 않는다.
|
|
|
|
## 5. 왜 `presentation`이 inbound adapter인가
|
|
|
|
React component는 사용자의 클릭, 입력, URL 이동을 해석한다. 그 결과를
|
|
application input port가 이해하는 command나 query로 바꾸고, 반환된
|
|
view state를 HTML로 표현한다. 이는 전형적인 driving/inbound adapter다.
|
|
|
|
```text
|
|
Button click
|
|
-> React event handler
|
|
-> CreateReferenceItem command
|
|
-> CreateReferenceItem input port
|
|
-> use case
|
|
```
|
|
|
|
따라서 `presentation`이 adapter라는 사실은 이름에 `adapter`가 붙었는지와
|
|
관계없다. 다음 두 구조 모두 가능하다.
|
|
|
|
```text
|
|
src/presentation # presentation을 inbound로 문서화
|
|
src/adapters/outbound
|
|
```
|
|
|
|
또는 더 명시적으로:
|
|
|
|
```text
|
|
src/adapters/inbound/react
|
|
src/adapters/outbound
|
|
```
|
|
|
|
이 저장소의 장기 목표는 두 번째 구조다. 점진적 전환 중에는
|
|
`presentation = inbound/react`로 동일하게 취급한다.
|
|
|
|
Presentation이 inbound라는 이유로 모든 component가 use case interface를
|
|
가져야 하는 것은 아니다. `Button`, `Card`, `Dialog` 같은 순수 UI primitive는
|
|
입력 props와 DOM behavior만 소유한다. page, controller hook, route
|
|
loader/action처럼 application 작업을 시작하는 경계에서 input port를
|
|
사용한다.
|
|
|
|
## 6. 브라우저가 서버 데이터 기술을 직접 연결하지 않는 이유
|
|
|
|
브라우저 adapter는 Redis, PostgreSQL, MongoDB, Kafka, MinIO 같은 서버
|
|
인프라에 직접 연결하지 않는다.
|
|
|
|
그 이유는 다음과 같다.
|
|
|
|
- 브라우저 bundle과 runtime config는 사용자가 읽을 수 있어 서버
|
|
credential을 안전하게 보관할 수 없다.
|
|
- 데이터베이스나 broker를 인터넷에 직접 노출하면 네트워크와 권한 경계가
|
|
무너진다.
|
|
- 브라우저는 신뢰할 수 없는 실행 환경이므로 authorization과 데이터
|
|
invariant를 강제할 수 없다.
|
|
- DB driver, broker protocol, connection pool과 migration은 브라우저의
|
|
lifecycle 및 bundle 제약과 맞지 않는다.
|
|
- 클라이언트 버전은 사용자마다 다를 수 있으므로 저장소 schema와 직접
|
|
결합하면 호환성 관리가 불가능해진다.
|
|
|
|
브라우저는 HTTPS API, BFF, GraphQL, WebSocket 또는 SSE처럼 서버가
|
|
노출한 제한된 계약과 통신한다.
|
|
|
|
```text
|
|
Browser
|
|
-> HTTP/WebSocket adapter
|
|
-> Backend API/BFF
|
|
-> Redis/PostgreSQL/MongoDB/Kafka/MinIO
|
|
```
|
|
|
|
MinIO object URL을 받아 파일을 업로드하는 경우에도 브라우저 adapter는
|
|
MinIO 관리자 credential이나 내부 API를 소유하지 않는다. 서버가 발급한
|
|
짧은 수명의 제한된 URL과 업로드 정책만 사용한다.
|
|
|
|
## 7. 권장 목표 디렉터리 구조
|
|
|
|
```text
|
|
src/
|
|
core/
|
|
kernel/
|
|
result.ts
|
|
failure.ts
|
|
identifiers.ts
|
|
domain/
|
|
application/
|
|
ports/
|
|
in/
|
|
out/
|
|
use-cases/
|
|
policies/
|
|
|
|
adapters/
|
|
inbound/
|
|
react/
|
|
app/
|
|
application-provider.tsx
|
|
app-router.tsx
|
|
routing/
|
|
query/
|
|
pages/
|
|
components/
|
|
design-system/
|
|
state/
|
|
outbound/
|
|
http/
|
|
fetch-transport.ts
|
|
request-builder.ts
|
|
response-decoder.ts
|
|
retry-decorator.ts
|
|
auth/
|
|
persistence/
|
|
observability/
|
|
platform/
|
|
|
|
features/
|
|
reference-feature/
|
|
domain/
|
|
application/
|
|
ports/
|
|
in/
|
|
out/
|
|
use-cases/
|
|
adapters/
|
|
inbound/
|
|
react/
|
|
outbound/
|
|
http/
|
|
contracts/
|
|
index.ts
|
|
|
|
bootstrap/
|
|
composition-root.ts
|
|
runtime-config.ts
|
|
main.tsx
|
|
```
|
|
|
|
`core`에는 모든 feature가 공유하는 작은 kernel과 truly cross-cutting
|
|
application capability만 둔다. `shared`, `common`, `utils` 같은 이름으로
|
|
기술과 업무 규칙을 무제한 혼합하지 않는다.
|
|
|
|
`features/reference-feature`는 구조를 설명하기 위한 완전한 수직
|
|
슬라이스다. 이 폴더를 삭제하고 route 등록 한 곳만 제거했을 때
|
|
typecheck, architecture check, test와 build가 모두 통과해야 한다.
|
|
|
|
## 8. Input port 설계 규칙
|
|
|
|
### 8.1 사용자의 의도를 이름으로 표현한다
|
|
|
|
좋은 예:
|
|
|
|
```ts
|
|
export interface ListReferenceItems {
|
|
execute(
|
|
query: ListReferenceItemsQuery,
|
|
context: RequestContext,
|
|
): Promise<Result<readonly ReferenceItemView[], ApplicationFailure>>;
|
|
}
|
|
```
|
|
|
|
피해야 할 예:
|
|
|
|
```ts
|
|
export interface HttpClient {
|
|
get(url: string): Promise<unknown>;
|
|
}
|
|
```
|
|
|
|
Input port는 “HTTP GET을 실행한다”가 아니라 “항목을 조회한다”를
|
|
표현한다.
|
|
|
|
### 8.2 React와 browser type을 포함하지 않는다
|
|
|
|
Input port에 다음 type을 넣지 않는다.
|
|
|
|
- `ReactNode`, `SyntheticEvent`
|
|
- `Request`, `Response`, `Headers`
|
|
- `QueryClient`, `UseQueryResult`
|
|
- `Location`, router navigate function
|
|
- `Storage`, `Window`, `Document`
|
|
|
|
Inbound adapter가 해당 type을 application command와 query로 변환한다.
|
|
|
|
### 8.3 예측 가능한 실패를 typed result로 반환한다
|
|
|
|
검증 실패, 인증 필요, conflict, network failure처럼 사용자 흐름에
|
|
포함되는 실패는 `Result<Value, Failure = AppFailure>`로 반환한다. programmer error와
|
|
불변식 위반을 모두 일반 API 실패로 숨기지는 않는다.
|
|
|
|
```ts
|
|
export type Result<Value, Failure = AppFailure> =
|
|
| Readonly<{ ok: true; value: Value }>
|
|
| Readonly<{ ok: false; error: Failure }>;
|
|
```
|
|
|
|
`AppFailure.kind`는 error registry의 key에서 파생한다. adapter가 받은 외부
|
|
오류 code는 이 닫힌 vocabulary로 매핑한 뒤 application 경계를 통과하며,
|
|
transport 문맥의 `ApiFailure`는 같은 type을 가리키는 호환 alias다.
|
|
|
|
### 8.4 Input port를 기술별로 합치지 않는다
|
|
|
|
`ApplicationService` 한 개에 모든 메서드를 계속 추가하지 않는다.
|
|
feature 단위 또는 응집된 capability 단위로 분리한다.
|
|
|
|
```ts
|
|
export interface ReferenceFeatureApplication {
|
|
readonly list: ListReferenceItems;
|
|
readonly create: CreateReferenceItem;
|
|
}
|
|
```
|
|
|
|
## 9. Output port 설계 규칙
|
|
|
|
### 9.1 Application이 port를 소유한다
|
|
|
|
도메인별 gateway는 feature의 application 경로에 둔다.
|
|
|
|
```ts
|
|
export interface ReferenceItemGateway {
|
|
list(
|
|
query: ListReferenceItemsQuery,
|
|
context: RequestContext,
|
|
): Promise<Result<readonly ReferenceItem, GatewayFailure>>;
|
|
|
|
create(
|
|
command: CreateReferenceItemCommand,
|
|
context: RequestContext,
|
|
): Promise<Result<ReferenceItem, GatewayFailure>>;
|
|
}
|
|
```
|
|
|
|
Fetch adapter는 이 interface를 구현하지만 application은 Fetch adapter를
|
|
import하지 않는다.
|
|
|
|
### 9.2 기술보다 capability를 표현한다
|
|
|
|
다음 이름을 우선한다.
|
|
|
|
- `ReferenceItemGateway`
|
|
- `SessionGateway`
|
|
- `PreferenceStore`
|
|
- `Clock`
|
|
- `IdGenerator`
|
|
- `Logger`
|
|
|
|
다음처럼 concrete 기술을 core port 이름에 포함하지 않는다.
|
|
|
|
- `AxiosPort`
|
|
- `LocalStoragePort`
|
|
- `RedisPort`
|
|
- `TanStackPort`
|
|
- `SentryPort`
|
|
|
|
### 9.3 Interface segregation을 지킨다
|
|
|
|
화면의 session 작업과 HTTP credential 부착은 호출 주체가 다르므로
|
|
분리한다.
|
|
|
|
```ts
|
|
export interface SessionGateway {
|
|
snapshot(): SessionSnapshot;
|
|
subscribe(listener: () => void): () => void;
|
|
beginSignIn(returnTo?: string): Promise<void>;
|
|
signOut(): Promise<void>;
|
|
recover(): Promise<SessionRecovery>;
|
|
}
|
|
|
|
export interface CredentialAttacher {
|
|
attach(request: Request): Promise<Request>;
|
|
}
|
|
```
|
|
|
|
`Request` type을 완전히 application 밖으로 유지하려면
|
|
`CredentialAttacher`는 outbound HTTP 내부 계약으로 둔다. Session use
|
|
case는 `SessionGateway`만 안다.
|
|
|
|
### 9.4 Clock과 ID도 concrete 구현과 분리한다
|
|
|
|
Application port 파일에 `Date.now`, `setTimeout`, `crypto.randomUUID`
|
|
구현을 함께 두지 않는다.
|
|
|
|
```text
|
|
core/application/ports/out/clock.ts
|
|
adapters/outbound/platform/browser-clock.ts
|
|
adapters/outbound/platform/browser-id-generator.ts
|
|
```
|
|
|
|
### 9.5 범용 HTTP client를 application port로 노출하지 않는다
|
|
|
|
범용 transport는 outbound adapter 내부의 재사용 기술이다. 각 feature의
|
|
gateway adapter가 transport를 사용해 DTO를 domain model로 매핑한다.
|
|
|
|
```text
|
|
Application -> ReferenceItemGateway
|
|
ReferenceItemHttpGateway -> HttpTransport
|
|
HttpTransport -> fetch
|
|
```
|
|
|
|
이렇게 하면 application이 URL, method, header, response envelope를 알지
|
|
않으며 API 기술을 교체할 수 있다.
|
|
|
|
## 10. Composition root와 `ApplicationProvider`
|
|
|
|
### 10.1 조립 순서
|
|
|
|
권장 조립 순서는 다음과 같다.
|
|
|
|
```text
|
|
runtime config 검증
|
|
-> release coherence 검증
|
|
-> platform adapters 생성
|
|
-> outbound gateways 생성
|
|
-> use cases 생성
|
|
-> application input API 생성
|
|
-> React infrastructure provider 생성
|
|
-> ApplicationProvider
|
|
-> Router
|
|
```
|
|
|
|
예:
|
|
|
|
```ts
|
|
export function createApplicationComposition(runtime: RuntimeConfig) {
|
|
const clock = createBrowserClock();
|
|
const logger = createRedactedLogger(runtime);
|
|
const sessionGateway = createExternalSessionGateway(runtime.auth);
|
|
const httpTransport = createHttpTransport({
|
|
baseUrl: runtime.apiBaseUrl,
|
|
timeoutMs: runtime.requestTimeoutMs,
|
|
maxRetryAttempts: runtime.maxRetryAttempts,
|
|
logger,
|
|
});
|
|
const referenceGateway = createReferenceItemHttpGateway(httpTransport);
|
|
|
|
const reference = Object.freeze({
|
|
list: createListReferenceItems({ gateway: referenceGateway, clock }),
|
|
create: createCreateReferenceItem({ gateway: referenceGateway, clock }),
|
|
});
|
|
|
|
const session = createSessionApplication({ gateway: sessionGateway, logger });
|
|
|
|
return Object.freeze({
|
|
application: Object.freeze({ reference, session }),
|
|
infrastructure: Object.freeze({
|
|
queryClient: createQueryClient(),
|
|
themeStorage: createThemeStorage(),
|
|
}),
|
|
});
|
|
}
|
|
```
|
|
|
|
### 10.2 `ApplicationProvider`가 노출할 것
|
|
|
|
`ApplicationProvider`는 presentation이 호출할 input port만 노출한다.
|
|
|
|
```ts
|
|
export interface ApplicationApi {
|
|
readonly reference: ReferenceFeatureApplication;
|
|
readonly session: SessionApplication;
|
|
}
|
|
```
|
|
|
|
다음 concrete 객체는 provider value에 넣지 않는다.
|
|
|
|
- Fetch client
|
|
- raw HTTP client
|
|
- browser storage
|
|
- telemetry SDK
|
|
- auth SDK owner
|
|
- domain gateway adapter
|
|
|
|
QueryClient처럼 React infrastructure provider가 직접 요구하는 concrete
|
|
객체는 bootstrap에서 해당 provider에 전달할 수 있다. 다만 feature page가
|
|
QueryClient를 직접 가져가 application을 우회하지 않도록 query bridge를
|
|
둔다.
|
|
|
|
### 10.3 현재 runtime의 교정 목표
|
|
|
|
현재 `composition.application`이 만들어지고도 사용되지 않는 상태를
|
|
허용하지 않는다. 다음 중 하나를 CI에서 검증한다.
|
|
|
|
- `main`은 `composition.application`을 `ApplicationProvider`에 전달한다.
|
|
- presentation production source는 `composition.ports`나 concrete
|
|
outbound adapter를 import하거나 props로 받지 않는다.
|
|
|
|
## 11. TanStack Query bridge
|
|
|
|
TanStack Query는 두 역할을 가진다.
|
|
|
|
1. QueryClient와 cache는 React 서버 상태 infrastructure다.
|
|
2. `useQuery`, `useMutation`은 React lifecycle을 application 호출에
|
|
연결하는 inbound bridge다.
|
|
|
|
따라서 application이 TanStack type을 알면 안 되지만, inbound React
|
|
adapter 내부의 feature hook은 TanStack을 사용할 수 있다.
|
|
|
|
```ts
|
|
export function useReferenceItems(query: ReferenceListQuery) {
|
|
const application = useApplication();
|
|
|
|
return useQuery({
|
|
queryKey: referenceQueryKeys.list(query),
|
|
queryFn: async ({ signal }) => {
|
|
const result = await application.reference.list.execute(query, {
|
|
routeId: "REFERENCE_LIST",
|
|
operationId: "LIST_REFERENCE_ITEMS",
|
|
signal,
|
|
});
|
|
|
|
if (!result.ok) {
|
|
throw toQueryFailure(result.error);
|
|
}
|
|
return result.value;
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
Page는 `useQuery`를 직접 조합하지 않고 feature hook을 사용한다.
|
|
|
|
```text
|
|
ReferenceListPage
|
|
-> useReferenceItems
|
|
-> ListReferenceItems input port
|
|
-> ReferenceItemGateway output port
|
|
```
|
|
|
|
Query bridge는 다음을 한곳에서 책임진다.
|
|
|
|
- canonical query key
|
|
- AbortSignal 전달
|
|
- application failure를 UI query failure로 투영
|
|
- stale/refreshing 상태 변환
|
|
- mutation 성공 후 namespace invalidation
|
|
- optimistic update와 rollback policy
|
|
- retry 소유권
|
|
|
|
HTTP transport가 retry를 소유하면 TanStack Query의 자동 retry는 기본적으로
|
|
끄고, 두 계층에서 중복 retry하지 않는다. UI의 “다시 시도” 버튼은 새
|
|
사용자 시도이며 transport의 자동 retry 횟수와 구분한다.
|
|
|
|
Application이 실제로 cache 일관성 자체를 업무 규칙으로 요구하는 경우에만
|
|
별도 `CachePort`를 둔다. 일반적인 server-state 표시를 위해 TanStack
|
|
Query의 모든 기능을 `read/write/invalidate` 세 메서드로 추상화하지 않는다.
|
|
|
|
## 12. 상태 관리 경계
|
|
|
|
모든 상태를 한 global store에 넣지 않는다.
|
|
|
|
| 상태 종류 | 기본 도구 | 저장 위치 |
|
|
| --- | --- | --- |
|
|
| 단일 component 표시 상태 | `useState`, `useReducer` | inbound React |
|
|
| URL과 공유 가능한 상태 | typed route params/search | router |
|
|
| 서버에서 소유한 상태 | TanStack Query bridge | inbound React infrastructure |
|
|
| session처럼 외부 store가 소유한 상태 | `useSyncExternalStore` | provider/bridge |
|
|
| theme 같은 작은 cross-page 설정 | context + preference port | provider/application |
|
|
| 복잡한 장기 client workflow | reducer 또는 state machine | feature application/inbound |
|
|
| 새로고침 후 유지할 공개 설정 | persistence output port | outbound persistence |
|
|
|
|
Zustand, Redux Toolkit, Jotai, XState 등은 실제 상태 복잡성이 확인될 때
|
|
선택한다. 기본 skeleton에는 상태 분류 규칙과 작은 typed external-store
|
|
예제가 필요하지만 범용 global store 의존성은 필수가 아니다.
|
|
|
|
Global store를 도입할 때도 domain invariant를 selector와 action 내부에
|
|
숨기지 않는다. 업무 규칙은 domain/application에 두고 store adapter는
|
|
입력과 표시 상태를 연결한다.
|
|
|
|
## 13. HTTP outbound adapter 계약
|
|
|
|
공통 HTTP transport가 지원해야 하는 최소 기능은 다음과 같다.
|
|
|
|
- base URL과 상대 path의 안전한 결합
|
|
- typed path parameter encoding
|
|
- canonical search parameter serialization
|
|
- parsed/normalized request body 전송
|
|
- Content-Type과 response envelope 검증
|
|
- operation별 auth 정책
|
|
- runtime 기본 timeout과 operation override
|
|
- AbortSignal과 navigation cancellation
|
|
- safe/keyed/non-idempotent retry 구분
|
|
- bounded exponential backoff와 `Retry-After`
|
|
- idempotency key의 logical request 단위 재사용
|
|
- 401 recovery 1회 제한
|
|
- request/trace ID의 안전한 투영
|
|
- raw request/response/token을 버리는 error normalization
|
|
- terminal failure logging과 telemetry
|
|
|
|
Operation registry는 문자열을 모아 둔 표가 아니라 TypeScript map으로
|
|
요청과 응답 type을 연결해야 한다.
|
|
|
|
```ts
|
|
interface ApiOperationMap {
|
|
LIST_REFERENCE_ITEMS: {
|
|
request: ListReferenceItemsRequest;
|
|
response: ReferenceItemListDto;
|
|
};
|
|
CREATE_REFERENCE_ITEM: {
|
|
request: CreateReferenceItemRequest;
|
|
response: ReferenceItemDto;
|
|
};
|
|
}
|
|
```
|
|
|
|
Zod 등 runtime schema가 반환한 `data`를 실제 request와 mapper 입력에
|
|
사용한다. `safeParse` 성공 여부만 확인하고 원본 값을 계속 사용하지 않는다.
|
|
|
|
## 14. 인증과 token 소유권
|
|
|
|
이 skeleton은 일반적인 browser token manager를 기본 제공하지 않는다.
|
|
이는 누락이 아니라 보안 기본값이다.
|
|
|
|
권장 순서는 다음과 같다.
|
|
|
|
1. 가능하면 BFF와 HttpOnly/Secure/SameSite cookie를 사용한다.
|
|
2. 외부 인증 SDK가 필요하면 SDK adapter가 token 획득, 갱신, 저장을
|
|
소유한다.
|
|
3. application과 presentation에는 credential이 아닌 session state와
|
|
sign-in/sign-out use case만 노출한다.
|
|
4. HTTP adapter는 opaque credential attachment capability만 사용한다.
|
|
5. localStorage나 일반 application store에 access/refresh token을 넣지
|
|
않는다.
|
|
|
|
SPA가 직접 bearer token을 사용해야 하는 프로젝트는 별도 보안 검토 후
|
|
memory 중심 token owner adapter를 구현한다. 그 경우에도 이 저장소의
|
|
`AUTH_TOKEN` storage 금지 계약을 우회하지 않는다.
|
|
|
|
## 15. Error, validation, logger 경계
|
|
|
|
### 15.1 Error
|
|
|
|
오류는 최소한 다음 경계를 거친다.
|
|
|
|
```text
|
|
unknown thrown value
|
|
-> outbound adapter normalization
|
|
-> typed application failure
|
|
-> query/controller projection
|
|
-> localized safe user message
|
|
```
|
|
|
|
UI에는 다음을 전달하지 않는다.
|
|
|
|
- raw stack
|
|
- backend message/details
|
|
- request/response body
|
|
- authorization header
|
|
- raw URL과 query
|
|
- storage value
|
|
|
|
### 15.2 Validation
|
|
|
|
Validation은 목적별로 분리한다.
|
|
|
|
| 위치 | 검증 대상 |
|
|
| --- | --- |
|
|
| bootstrap | runtime config, release manifest |
|
|
| route inbound adapter | params와 search |
|
|
| form/controller | 사용자 입력과 field feedback |
|
|
| outbound API adapter | request DTO와 response DTO |
|
|
| domain | invariant와 value object |
|
|
|
|
Form schema와 API request schema가 같은 모양이어도 역할이 다르므로
|
|
무조건 하나로 합치지 않는다. 필요한 경우 application command로
|
|
명시적으로 변환한다.
|
|
|
|
### 15.3 Logger와 telemetry
|
|
|
|
Logger와 telemetry는 같은 것이 아니다.
|
|
|
|
- Logger: 운영 진단을 위한 level 기반 structured record
|
|
- Telemetry: registry에 정의된 semantic event와 metric
|
|
- Error reporter: 예외 집계와 release correlation
|
|
|
|
VD-07에 따라 기본 구현은 임의 message 문자열을 받는 `Logger`가 아니라 닫힌
|
|
event ID와 safe context만 받는 `DiagnosticsPort`다.
|
|
|
|
```ts
|
|
export interface DiagnosticsPort {
|
|
record(input: {
|
|
level: DiagnosticLevel;
|
|
eventId: DiagnosticEventId;
|
|
context?: DiagnosticContext;
|
|
}): void;
|
|
}
|
|
```
|
|
|
|
기본 runtime에는 bounded in-memory diagnostics와 설정 기반 best-effort
|
|
telemetry adapter, 테스트에는 recording 또는 no-op adapter를 연결한다.
|
|
direct `console`은 redaction 경계를 우회하므로 source gate가 거절한다.
|
|
민감정보 redaction은 각 호출자의 선의가 아니라 adapter와 contract에서
|
|
강제한다.
|
|
|
|
현재 production producer는 boot, logical HTTP outcome, cache, storage, route,
|
|
render, release mismatch와 telemetry delivery drop을 포함한다. HTTP terminal
|
|
telemetry는 모든 retry가 끝난 뒤 한 번만 발행하며 abort에는 발행하지 않는다.
|
|
raw path/query/body/error 대신 route/operation/correlation ID와
|
|
status/attempt/duration bucket만 전달한다.
|
|
|
|
## 16. Feature 경계와 removable reference feature
|
|
|
|
Reference feature는 단순 UI fixture가 아니라 다음 경로를 모두 실행해야
|
|
한다.
|
|
|
|
```text
|
|
registered route
|
|
-> lazy page
|
|
-> controller/query bridge
|
|
-> application input port
|
|
-> use case
|
|
-> output gateway port
|
|
-> HTTP adapter
|
|
-> request schema
|
|
-> response schema
|
|
-> DTO mapper
|
|
-> domain model
|
|
-> view model
|
|
-> loading/empty/success/error UI
|
|
```
|
|
|
|
Reference feature 폴더가 소유해야 할 항목:
|
|
|
|
- domain model과 value object
|
|
- input/output port
|
|
- use case
|
|
- API operation과 DTO schema
|
|
- HTTP gateway와 mapper
|
|
- query key와 query/mutation bridge
|
|
- route page와 feature component
|
|
- unit/component/integration/E2E test
|
|
|
|
공통 registry가 필요한 경우 reference feature가 registration object를
|
|
export하고 bootstrap 또는 route composition이 이를 수집한다. sample
|
|
operation을 전역 core registry 안에 하드코딩하지 않는다.
|
|
|
|
제거 테스트는 다음을 수행해야 한다.
|
|
|
|
1. `src/features/reference-feature` 전체 삭제
|
|
2. reference route registration 삭제
|
|
3. reference test 삭제 또는 제외
|
|
4. 잔여 import와 registry owner 검색
|
|
5. typecheck
|
|
6. architecture check
|
|
7. production build
|
|
8. built asset에서 reference operation/schema 문자열 부재 확인
|
|
|
|
제품 feature는 reference feature를 import할 수 없다. Reference feature도
|
|
제품에서만 존재하는 feature를 import하지 않는다.
|
|
|
|
## 17. 필수 adapter catalog
|
|
|
|
| Adapter | 책임 | 기본 구현 |
|
|
| --- | --- | --- |
|
|
| Runtime config loader | public runtime config fetch와 검증 | Fetch + Zod |
|
|
| Release manifest loader | build/release coherence 확인 | Fetch + Zod |
|
|
| Browser clock | 현재 시간과 abortable sleep | browser timer |
|
|
| ID generator | idempotency/correlation용 opaque ID | `crypto.randomUUID` |
|
|
| HTTP transport | request, timeout, retry, decode | Fetch |
|
|
| Domain API gateway | DTO와 domain 변환 | feature HTTP gateway |
|
|
| Session gateway | session snapshot와 login lifecycle | external owner |
|
|
| Credential attacher | opaque request credential 처리 | auth owner |
|
|
| Preference store | 공개 preference persistence | browser storage |
|
|
| Query infrastructure | server-state cache 정책 | TanStack Query |
|
|
| Query bridge | input port와 React query 연결 | feature hook |
|
|
| Logger | redacted structured diagnostics | console/no-op/remote |
|
|
| Telemetry | allowlist semantic event 전달 | best-effort sink |
|
|
| Browser lifecycle | visibility/pagehide/online 상태 | browser event adapter |
|
|
| Router | URL을 page/controller로 변환 | React Router |
|
|
| Design system | 접근 가능한 primitive와 token | React/CSS |
|
|
| Locale/formatter | message key와 날짜·숫자·방향성 | local locale facade |
|
|
| UI workshop/visual gate | 격리 상태·interaction·회귀 검증 | Storybook + local Playwright |
|
|
|
|
필수라는 의미는 모든 concrete library를 고정한다는 뜻이 아니다. 해당
|
|
capability의 기본 정책, port 또는 안전한 no-op 구현과 composition
|
|
위치가 정의되어 있어야 한다는 뜻이다.
|
|
|
|
## 18. 선택 adapter catalog
|
|
|
|
| Adapter | 도입 조건 | 기본 상태 |
|
|
| --- | --- | --- |
|
|
| SSE/WebSocket/bounded polling | active document의 server event 또는 duplex protocol 필요 | common target `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` |
|
|
| Web Push | inactive browser의 user-visible notification 필요 | target `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` |
|
|
| IndexedDB/OPFS | 큰 offline data, durable queue 또는 large local object 필요 | native reference runtime 제공, 미조립 |
|
|
| Cache Storage | 승인된 public HTTP representation offline cache 필요 | native reference runtime 제공, 미조립 |
|
|
| Service Worker/PWA | offline shell과 installability 필요 | lifecycle recipe 제공, 미설치 |
|
|
| Offline mutation queue | 재연결 후 명령 재처리 필요 | 미설치 recipe |
|
|
| Feature flag | remote rollout/kill switch 필요 | opt-in recipe 제공, 미설치 |
|
|
| Translation catalog vendor | 원격 catalog·복수 namespace 운영 필요 | 기본 locale facade 뒤에 미설치 |
|
|
| Analytics | 사용자 동의 기반 product analytics 필요 | opt-in recipe 제공, 미설치 |
|
|
| Error-reporting SDK | 운영 예외 집계 필요 | opt-in recipe 제공, 미설치 |
|
|
| OpenTelemetry | 조직 trace 연계 필요 | opt-in recipe 제공, 미설치 |
|
|
| Web Worker | CPU 작업이 main thread를 막음 | opt-in recipe 제공, 미설치 |
|
|
| Notification | 사용자 권한 기반 browser notification 필요 | opt-in recipe 제공, 미설치 |
|
|
| File/Blob/picker/download | 해당 file workflow 필요 | native reference runtime 제공, 미조립 |
|
|
| Clipboard/Media | 해당 browser capability 필요 | opt-in recipe 제공, 미설치 |
|
|
| Image CDN adapter | responsive image transform 필요 | 미설치 recipe |
|
|
| Virtualization | 대량 list rendering이 측정상 병목 | opt-in recipe 제공, 미설치 |
|
|
| OpenAPI generator | backend 계약에서 client 생성 필요 | opt-in recipe 제공, 미설치 |
|
|
| Zustand/Redux/Jotai | 복잡한 cross-page client state 확인 | opt-in recipe 제공, 미설치 |
|
|
| XState 등 state machine | 장기 workflow 상태 전이가 복잡함 | opt-in recipe 제공, 미설치 |
|
|
| Cloud visual-review service | 외부 승인·호스팅 workflow 필요 | 로컬 Storybook/visual gate 뒤에 미설치 |
|
|
|
|
선택 adapter는 “나중에 쓸 수 있으므로” 기본 bundle에 넣지 않는다. 도입
|
|
조건, 보안 영향, bundle 비용과 제거 방법이 확인된 경우에만 추가한다.
|
|
SSE, WebSocket, Web Push와 bounded polling의 서로 다른 delivery 의미,
|
|
inbound/outbound 분리, resume·gap·lifecycle과 현재 상태는
|
|
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)을
|
|
따른다.
|
|
Web Push는 subscription/provider/worker delivery를, 별도 Notification 행은
|
|
permission과 user-visible rendering facade를 뜻한다. bounded polling은 inbound
|
|
push adapter가 아니라 Query bridge 또는 application orchestrator가 기존 HTTP
|
|
operation을 schedule하는 policy다.
|
|
현재 구현된 공통 catalog, TypeScript contract/fake, browser-native reference
|
|
runtime과 blocking gate는
|
|
`docs/architecture/optional-adapter-recipes.md`와
|
|
`config/recipes/frontend-capability-recipes.json`을 따른다. 이 recipe source를
|
|
production에서 직접 import하는 것은 금지하며 선택한 contract만 application
|
|
소유 경계로 이동한다. 실제 API를 호출하는 `referenceRuntime`은 catalog에
|
|
`sourceRoots`가 등록된 browser file/IndexedDB/OPFS/Cache/transfer 계열에만
|
|
존재한다. realtime target은 아직 `DESIGNED_NOT_IMPLEMENTED`다. 구현된
|
|
reference runtime도 dataset/schema/codec/query/policy와 제품 owner가 없으면
|
|
bootstrap에 연결하지 않는다.
|
|
|
|
## 19. 새 outbound adapter 추가 recipe
|
|
|
|
### 단계 1: 해결할 문제를 capability로 정의한다
|
|
|
|
예: “LaunchDarkly를 붙인다”가 아니라 “feature flag를 평가한다”로
|
|
정의한다.
|
|
|
|
### 단계 2: Port가 정말 필요한지 판단한다
|
|
|
|
- application 정책이 이 capability를 호출하는가?
|
|
- React 화면 한 곳의 순수 표현 문제인가?
|
|
- 기존 port로 충분한가?
|
|
- 기술 교체와 테스트 격리가 실제로 필요한가?
|
|
|
|
Application이 호출하지 않는 순수 UI library에는 억지 output port를
|
|
만들지 않는다.
|
|
|
|
### 단계 3: Application-owned output port를 작성한다
|
|
|
|
- 기술 중립 이름
|
|
- 최소 메서드
|
|
- typed input/output
|
|
- timeout/cancellation 필요 여부
|
|
- 예측 가능한 failure union
|
|
- 민감정보 분류
|
|
|
|
### 단계 4: Contract와 정책을 작성한다
|
|
|
|
- runtime config
|
|
- retry/idempotency
|
|
- cache와 TTL
|
|
- fallback
|
|
- telemetry
|
|
- security/redaction
|
|
- browser compatibility
|
|
|
|
### 단계 5: Concrete adapter를 구현한다
|
|
|
|
`src/adapters/outbound/<capability>` 또는 feature의
|
|
`adapters/outbound`에 구현한다. Concrete SDK type은 이 경로 밖으로
|
|
노출하지 않는다.
|
|
|
|
### 단계 6: Composition root에서만 연결한다
|
|
|
|
Application이나 page에서 concrete constructor를 호출하지 않는다.
|
|
|
|
### 단계 7: 테스트한다
|
|
|
|
- port contract test
|
|
- adapter unit test
|
|
- failure normalization
|
|
- timeout/cancellation
|
|
- redaction
|
|
- integration test
|
|
- deliberately failing negative fixture
|
|
|
|
### 단계 8: 운영 계약을 갱신한다
|
|
|
|
- registry와 runtime schema
|
|
- dependency inventory
|
|
- bundle budget
|
|
- security policy
|
|
- runbook
|
|
- adapter catalog와 제거 절차
|
|
|
|
## 20. 새 inbound adapter 추가 recipe
|
|
|
|
외부 이벤트를 application에 전달하는 adapter는 다음 순서를 따른다.
|
|
|
|
1. 어떤 외부 event가 어떤 application 의도를 나타내는지 정의한다.
|
|
2. event payload를 runtime schema로 검증한다.
|
|
3. raw payload를 application command로 매핑한다.
|
|
4. input port를 호출한다.
|
|
5. duplicate, ordering, cancellation 정책을 정의한다.
|
|
6. 실패를 raw event source로 무한 재전파하지 않는다.
|
|
7. connect/disconnect lifecycle과 subscription cleanup을 테스트한다.
|
|
|
|
예를 들어 WebSocket listener가 받은 JSON을 domain object로 바로
|
|
사용하지 않는다. schema 검증과 command mapping 후 input port를 호출한다.
|
|
|
|
## 21. 새 feature 추가 recipe
|
|
|
|
### 단계 1: Feature 경계를 선언한다
|
|
|
|
- feature 이름과 owner
|
|
- 사용자 목표
|
|
- route
|
|
- 입력 command/query
|
|
- 외부 gateway
|
|
- 성공/빈 화면/오류/권한 상태
|
|
|
|
### 단계 2: Domain을 작성한다
|
|
|
|
- entity/value object
|
|
- invariant
|
|
- 순수 policy
|
|
- framework와 transport type 금지
|
|
|
|
Domain 규칙이 없는 단순 표시 feature라면 빈 domain layer를 억지로
|
|
만들지 않는다.
|
|
|
|
### 단계 3: Input/output port와 use case를 작성한다
|
|
|
|
- input port는 사용자 의도
|
|
- output port는 외부 capability
|
|
- `Result` failure 정의
|
|
- cancellation context 정의
|
|
|
|
### 단계 4: Outbound gateway를 작성한다
|
|
|
|
- operation registration
|
|
- request DTO schema
|
|
- path/search/body mapping
|
|
- response DTO schema
|
|
- domain mapper
|
|
- error mapping
|
|
|
|
### 단계 5: Inbound query/controller bridge를 작성한다
|
|
|
|
- canonical query key
|
|
- application input 호출
|
|
- async state projection
|
|
- mutation invalidation 또는 optimistic rollback
|
|
- retry 버튼 behavior
|
|
|
|
### 단계 6: Route를 등록한다
|
|
|
|
- route ID와 path
|
|
- typed params/search
|
|
- access hint
|
|
- lazy chunk
|
|
- loading/error surface
|
|
- title/navigation
|
|
|
|
Registry metadata와 실제 route object를 별도로 두 번 작성하지 않는다.
|
|
하나의 typed definition에서 route object와 navigation을 생성한다.
|
|
|
|
### 단계 7: Page와 상태를 조립한다
|
|
|
|
- page header와 focus 이동
|
|
- loading
|
|
- empty
|
|
- success
|
|
- refreshing/stale
|
|
- terminal error
|
|
- 401/403/404
|
|
- mutation pending/conflict
|
|
- responsive/keyboard/screen-reader
|
|
|
|
### 단계 8: 테스트한다
|
|
|
|
- domain/use case unit
|
|
- port contract
|
|
- outbound adapter + MSW integration
|
|
- query/controller hook
|
|
- component
|
|
- route integration
|
|
- Playwright E2E
|
|
- axe와 수동 접근성 scope
|
|
|
|
### 단계 9: 제거 가능성을 확인한다
|
|
|
|
Reference feature라면 전체 폴더 삭제 후 build가 통과해야 한다. 제품
|
|
feature라면 다른 feature가 내부 구현 경로를 직접 import하지 않고 public
|
|
entry point만 사용해야 한다.
|
|
|
|
## 22. Routing 규칙
|
|
|
|
Route definition은 다음 값을 type-safe하게 결합한다.
|
|
|
|
- route ID
|
|
- path와 parameter type
|
|
- search schema와 normalized type
|
|
- access hint
|
|
- lazy component
|
|
- loading surface
|
|
- error boundary
|
|
- title
|
|
- navigation metadata
|
|
- prefetch policy
|
|
|
|
Client route access는 UX 정책일 뿐 authorization이 아니다. 서버가
|
|
최종 권한을 검증해야 한다.
|
|
|
|
다음 규칙을 적용한다.
|
|
|
|
- string path를 page에 하드코딩하지 않는다.
|
|
- URL search를 `Object.fromEntries` 결과 그대로 신뢰하지 않는다.
|
|
- route parameter를 domain ID로 사용하기 전에 검증한다.
|
|
- route 전환 시 진행 중 request에 AbortSignal을 전달한다.
|
|
- chunk load 실패는 release mismatch 정책에 따라 한 번만 복구한다.
|
|
- redirect loop guard는 실제 redirect 경로에 연결하거나 제거한다.
|
|
- browser-history routing을 사용하면 hosting의 SPA fallback을 release 계약으로
|
|
검증한다.
|
|
|
|
## 23. Design system 경계
|
|
|
|
Design system은 inbound React adapter의 공유 UI capability다.
|
|
|
|
최소 public surface:
|
|
|
|
- semantic token
|
|
- typography와 spacing
|
|
- Button, IconButton
|
|
- TextField, TextArea, Select
|
|
- Checkbox, Radio, Switch
|
|
- Card, Alert, Badge
|
|
- Dialog, Drawer
|
|
- Loading, Empty, Error, Forbidden surface
|
|
- Tooltip, Tabs, Toast
|
|
- Table/List, Pagination
|
|
- Form field와 validation message
|
|
|
|
Icon library는 design-system의 `Icon`과 `IconButton` 뒤에서 사용한다.
|
|
Page가 `lucide-react` 등 concrete icon package를 무제한 import하지 않도록
|
|
허용 icon과 접근성 규칙을 한곳에서 관리한다. 정적 named import로
|
|
tree-shaking을 유지하고 전체 icon registry를 runtime dynamic import하지
|
|
않는다.
|
|
|
|
Design system은 domain use case를 호출하지 않는다. Feature component가
|
|
design-system primitive를 조합하고 controller hook을 통해 application을
|
|
호출한다.
|
|
|
|
## 24. 금지 패턴
|
|
|
|
다음 패턴은 architecture gate에서 차단해야 한다.
|
|
|
|
### 경계 우회
|
|
|
|
- page가 `src/adapters/outbound`를 import
|
|
- presentation이 raw `fetch`, `localStorage`, auth SDK를 직접 사용
|
|
- application이 React, Router, TanStack Query를 import
|
|
- domain이 application, browser global 또는 framework를 import
|
|
- bootstrap 외부에서 concrete adapter를 생성
|
|
- `composition.ports`를 product presentation에 직접 전달
|
|
|
|
### Port 오용
|
|
|
|
- application port에 Axios, TanStack, SDK type 노출
|
|
- input port 이름을 HTTP method나 UI event 이름으로 정의
|
|
- 모든 capability를 하나의 `ApplicationService` 또는 `StorePort`로 합침
|
|
- auth UI operation과 credential attachment를 하나의 과대 interface로
|
|
강제
|
|
- application이 raw cache read/write를 page에 재노출
|
|
|
|
### HTTP와 retry
|
|
|
|
- GET filter를 cache key에만 반영하고 request에서 누락
|
|
- schema parse 결과를 버리고 원본 body 전송
|
|
- keyed mutation retry 시 idempotency key 재생성
|
|
- HTTP와 TanStack이 동시에 자동 retry
|
|
- navigation abort를 unknown error로 기록
|
|
- validation early return에서 timeout/listener cleanup 누락
|
|
|
|
### 상태
|
|
|
|
- 서버 상태를 generic global store에 복제
|
|
- URL에 있어야 할 filter를 별도 store에만 보관
|
|
- token을 React state, persisted store, localStorage에 저장
|
|
- domain invariant를 Zustand/Redux action 내부에만 구현
|
|
|
|
### 오류와 관측
|
|
|
|
- raw backend message 또는 stack을 사용자에게 표시
|
|
- request/response body, raw URL, token을 logger/telemetry에 전달
|
|
- `console.error`를 production error strategy로 간주
|
|
- registry에 event만 선언하고 실제 발생 경로에서 emit하지 않음
|
|
|
|
### Registry와 예제
|
|
|
|
- route metadata를 선언하고 실제 route가 사용하지 않음
|
|
- `contracts`를 모든 계층이 자유롭게 import하는 우회 폴더로 사용
|
|
- product가 reference feature를 import
|
|
- reference feature를 삭제해도 sample domain/API schema가 초기 bundle에
|
|
남음
|
|
|
|
### 외부 인프라
|
|
|
|
- 브라우저에서 Redis, PostgreSQL, MongoDB, Kafka, MinIO 관리자 API에
|
|
직접 연결
|
|
- server credential을 build/runtime config에 포함
|
|
- 클라이언트 route guard를 authorization으로 간주
|
|
|
|
## 25. 테스트와 증적 기준
|
|
|
|
경계별 최소 테스트는 다음과 같다.
|
|
|
|
| 대상 | 최소 검증 |
|
|
| --- | --- |
|
|
| Input use case | 성공, 각 failure branch, cancellation |
|
|
| Output port | contract fixture와 negative fixture |
|
|
| HTTP transport | path/query/body, timeout, retry, 401, abort, schema |
|
|
| DTO mapper | valid mapping과 invariant breach normalization |
|
|
| Query bridge | query key, loading, refresh, mutation invalidation, rollback |
|
|
| Route | registry-tree 일치, params/search, access, 404 |
|
|
| Provider | 실제 composition application 주입 |
|
|
| Logger/telemetry | allowlist, redaction, sink failure |
|
|
| Design system | keyboard, focus, label/error association, axe |
|
|
| Reference feature | full vertical integration과 complete removal |
|
|
|
|
TypeScript 전환 후에는 source뿐 아니라 test와 architecture/security
|
|
fixture도 typecheck 또는 lint 대상이어야 한다. 모든 실행 fixture를
|
|
`.ts/.tsx`로 유지해 확장자별 검사 우회를 허용하지 않는다.
|
|
|
|
Coverage는 단순 report 생성이 아니라 branch/function/line threshold를
|
|
blocking gate로 둔다. 수치만 올리기 위한 구현 세부 테스트보다 port
|
|
contract와 실패 분기를 우선한다.
|
|
|
|
## 26. 완료 기준
|
|
|
|
### 26.1 Architecture
|
|
|
|
- [ ] 모든 application 작업에 명시적인 input port가 있다.
|
|
- [ ] 모든 외부 capability는 application-owned output port 뒤에 있다.
|
|
- [ ] Presentation은 concrete outbound adapter를 모른다.
|
|
- [ ] `main`은 raw ports 대신 application API를 제공한다.
|
|
- [ ] `contracts`는 unrestricted 우회 계층이 아니다.
|
|
- [ ] dependency rule과 문서의 source of truth가 하나다.
|
|
- [x] TypeScript와 TSX도 동일한 architecture/security lint를 받는다.
|
|
|
|
### 26.2 Runtime composition
|
|
|
|
- [x] runtime timeout/retry 설정이 실제 HTTP transport에 반영된다.
|
|
- [x] QueryClient와 feature query bridge가 실제 route에서 동작한다.
|
|
- [x] session UI API와 credential attachment가 분리되어 있다.
|
|
- [x] diagnostics와 telemetry가 HTTP/render/storage/cache failure 경로에
|
|
연결된다.
|
|
- [x] `pagehide`에서 bounded telemetry queue를 flush하고 adapter `dispose`가
|
|
lifecycle listener를 정리한다.
|
|
|
|
### 26.3 HTTP와 validation
|
|
|
|
- [ ] path, search, body를 각각 검증하고 직렬화한다.
|
|
- [x] schema가 반환한 normalized data를 실제 request에 사용한다.
|
|
- [x] 생성된 timeout과 AbortSignal listener가 현재 HTTP attempt의 terminal
|
|
경로에서 정리된다.
|
|
- [ ] retry는 runtime cap, idempotency와 `Retry-After`를 따른다.
|
|
- [ ] raw payload와 credential이 failure나 log에 포함되지 않는다.
|
|
|
|
### 26.4 Routing과 상태
|
|
|
|
- [x] route registry와 실행 route tree가 동일 source에서 생성된다.
|
|
- [x] params/search schema가 실제 navigation에서 실행된다.
|
|
- [x] loading/error/chunk/access metadata가 실행 behavior와 연결된다.
|
|
- [ ] local, URL, server, session, persisted state가 분류 규칙을 따른다.
|
|
- [x] reference server state를 별도 global store에 중복 보관하지 않는다.
|
|
|
|
### 26.5 Reference feature
|
|
|
|
- [x] route부터 API mapper와 화면까지 완전한 수직 경로가 실행된다.
|
|
- [x] list/create 등 최소 query와 mutation 예제가 있다.
|
|
- [x] loading/empty/error/refresh/conflict 상태가 있다.
|
|
- [x] reference feature 전체 삭제 후 typecheck/architecture/registry/test/home/build가 통과한다.
|
|
- [x] 제거 모드 built asset에 reference operation, schema, mapper가 남지 않는다.
|
|
|
|
### 26.6 품질
|
|
|
|
- [x] source와 test가 strict TypeScript 검사를 받는다.
|
|
- [ ] React Hooks와 JSX accessibility lint가 blocking이다.
|
|
- [ ] coverage threshold가 blocking이다.
|
|
- [ ] MSW integration, component, 3-engine E2E와 axe가 통과한다.
|
|
- [ ] bundle budget과 dependency inventory가 갱신된다.
|
|
- [ ] 선택 adapter는 도입 조건과 제거 절차가 문서화되어 있다.
|
|
|
|
## 27. 구현 우선순위
|
|
|
|
### P0: 경계와 정확성
|
|
|
|
1. TypeScript 검사 도구와 `.ts/.tsx` architecture/security glob 준비
|
|
2. input/output port 분리와 `ApplicationProvider`
|
|
3. HTTP path/query/body, parsed data, runtime retry/timeout, cleanup 교정
|
|
4. Query bridge를 통한 실제 application 실행
|
|
5. typed routing, registry-tree 단일화와 chunk recovery 실행 연결
|
|
6. 완전한 removable reference feature와 제거 검증
|
|
|
|
### P1: 기본 플랫폼 완성도
|
|
|
|
1. 완료: error/result/validation/form kernel
|
|
2. 완료: 배타적인 async 상태와 page template
|
|
3. Logger와 telemetry 실제 wiring
|
|
4. design-system public API, icon wrapper와 headless interaction
|
|
5. locale/message/formatter와 pseudo-locale/RTL 경계
|
|
6. Storybook, shared MSW, 시각 회귀와 built-output E2E
|
|
7. source/test TypeScript 전환 및 위험 기반 coverage/lint 강화
|
|
|
|
### P2: 프로젝트별 선택 capability
|
|
|
|
SSE/WebSocket, Web Push, bounded polling, offline/IndexedDB, Service Worker, feature flag, product
|
|
analytics, vendor error reporting, worker, virtualization, OpenAPI generation,
|
|
GraphQL, Connect/gRPC-Web, Protobuf REST Gateway, global store와 cloud
|
|
visual-review service는 실제 프로젝트 요구와 측정 결과에 따라 추가한다.
|
|
GraphQL과 browser Protobuf/Gateway의 선택·설치 조건은 VD-26과
|
|
VD-27/VD-29/VD-30을 따른다.
|
|
|
|
P2 adapter를 많이 설치하는 것은 skeleton 완성도의 기준이 아니다.
|
|
안전한 경계, 도입 recipe, 테스트 계약과 제거 가능성이 준비되어 있는지가
|
|
기준이다.
|