Skip to content

저장·입력·포커스 의미를 공통 계약으로 — v0.18.0 U01 UI 기반 (2026-09-07)

  • 기간: 2026-09-07, 세션 01a0774b-ecb1-7fa0-a15d-7069d7a7167a. 오너 지시: “1289 진행해줘”.
  • 랜딩: #1289 완료·인계 댓글에서 PR·검증 head·main merge SHA·staging 실행을 추적한다. 작업 브랜치 codex/ui/u01-contracts-1289, 기준 main 16b8e99684a4cb152b2ff5d89b5aea10a7f055e0. 마이그레이션·Edge 변경 없음. Production 릴리스는 R06에서 별도로 수행한다.
  • 설계서: 별도 artifact 없음. 착수 범위와 아래 정본 계약이 설계 입력이다.
  • 정본: props·접근성, 시각 기반, UI_PRIMITIVES_CONTRACT_VERSION = 1, ui/shared/primitives/**.
  • 도구: 테스트 전용 tests/browser/u01/server.mjs·Playwright config. 앱·DB·외부 인증 없이 실제 컴포넌트를 불러오는 브라우저 행동 하니스이며 제품 빌드 진입점이 아니다.
  • 게이트: 신규 Node 10건, 실제 Edge 16건, 기존 frontend/contracts import boundary·CSS·lint/type/test-manifest. 전체 로컬/CI 결과와 SHA는 #1289 최종 댓글에서 구분한다. DB/pgTAP 변경이 없어 이 트랙에서 새 DB 재생 검증은 요구하지 않는다.
  • 계약: U01 v1 신규, G03 core v1 유지. 기존 쓰기 파이프라인·하단 크롬 정책 유지.
  • 총괄: v0.18.0 U01. 총괄 상태·Phase 집계는 HQ가 별도 통합한다.

1. 배경

G03이 입력 셀과 기기 보관·서버 확정·통계 발행에 이름을 붙였다. U02/U03가 모바일과 데스크톱 화면을 이 계약으로 옮기기 전에, 같은 상태를 어떻게 보여 주고 입력/키보드/포커스를 어떻게 다룰지 공통 입력이 필요했다. 이슈 #1289는 전체 화면 재설계가 아니라 그 표현 계약과 작은 대표 구현을 요구한다.

2. 문제 제기

같은 저장 완료라는 말이 서로 다른 완료 지점을 가리킬 수 있었다

기기 대기열에 적힌 시점, 서버 영수증을 받은 시점, 통계 숫자가 따라온 시점은 다르다. 화면이 receipt나 online 상태에서 이를 추측하면 실제 저장 상태와 문구가 갈라진다. unknown을 성공으로 올리는 추측도 차단해야 했다.

플랫폼마다 입력·다이얼로그 껍데기가 달랐다

모바일 UiModal은 .app root portal로 clipping을 해결하고 데스크톱 DkScrim은 문서 Escape 리스너를 썼다. shared에는 버튼·입력·dialog의 포커스·IME·오류 연결을 묶는 작은 primitive가 없었다. 기존 껍데기를 한 번에 치환하면 스택·native back·레이아웃까지 변하게 된다.

이미 있는 CSS layer와 플랫폼 독립 token을 존중해야 했다

기존 top-level layer 5개와 의도적인 host unlayered 규칙, 모바일 하단 clearance 유도식이 있다. 이를 무시하고 새 전역 palette나 z-index 숫자만 보태면 기존 플랫폼 경계가 약해진다.

3. 해결 방안

원칙

  • D1: 오너의 “1289 진행해줘” 지시에 따라 U01 소유 범위의 공통 계약과 대표 구현을 진행한다.
  • D2: G03 core v1의 입력·저장 의미를 소비한다. core·수치·저장 정책을 변경하지 않는다.
  • D3: 기존 모바일/데스크톱 표현은 독립적으로 유지한다. 전체 채택은 U02/U03, CSS 정리는 U07, 최종 화면 검증은 U06이다.

접근

선택이유
React 독립 표시 모델 + UI 로컬 DOM props계약 층이 React를 import하지 않는 G03 경계를 유지
작은 신규 primitive + 오류 gate 버튼 대표 연결기존 화면 배치를 유지하며 공통 동작을 실제 production 경로에서 소비
소비 요소의 semantic alias + 기존 base sublayer플랫폼 값 독립과 기존 CSS 우선순위를 보존
native dialog top layer + 명시적 Tab 경계 순환배경 inert·clipping 해소를 브라우저가 담당하고 실제 관측한 Tab 이탈은 보완

4. 적용한 내용

계약과 대표 구현

  • 표시 모델 3종: UiInputModel, UiStateModel, UiPersistenceModel. G03 ViewModel 브랜드와 기존 상태 이름을 재사용한다.
  • UiButton: 기본 button, native disabled, focus를 유지하는 pending, click/Enter/Space 중복 intent 차단.
  • UiInput: raw 문자열 보존, controlled value, label/hint/error 연결, composition/229 Enter 차단. 숫자/검색/저장 판단은 binding 소유다.
  • UiDialog: modal, 제목/설명, 초기/복귀 ref, Tab 순환, IME Escape 보호, controlled dismiss, unmount 복귀.
  • UiState·UiPersistenceStatus: loading/empty/error와 저장 7가지 표현. 라이브 영역은 실제 상태 메시지에만 적용한다.
  • DataErrorGate 재시도 1개, AppErrorScreen 동작 2개를 UiButton으로 연결했다. DOM 배치·기존 클래스·문구·callback은 유지했다.

주요 결정과 근거

새 모델에는 outbox 행·receipt·generation을 넣지 않는다. UI는 표현 결론만 소비하므로 서버 확정/통계 발행을 직접 계산할 재료가 없다. 저장 단계의 표시 표는 새로운 toast 정책이 아니며 기존 적재 즉시 진행·성공 무음 규칙을 그대로 유지한다.

새 클래스와 state selector는 시각 계약에 닫힌 목록으로 남겼다. 기존 host unlayered·mobile root portal·desktop scrim·bottomChrome·important 예외는 책임과 제거 조건을 함께 기록했다. CSS dead-code allowlist는 늘리지 않았다.

작업 중 드러난 것

  • 최초 boundary 검사에서 contracts의 React type import를 발견했다. React/DOM props를 각 primitive로 옮기고 표시 모델만 계약 층에 남긴 뒤 34개 관련 검사를 통과했다.
  • 최초 브라우저 실행은 하니스 상대 경로가 한 단계 깊어 초기 3개 검사가 실행되지 못했다. 경로 수정 뒤 전체를 새 실행했다.
  • 실제 native dialog에서 마지막 Tab이 browser chrome 쪽으로 이동해 내부 focus 검사 2건이 실패했다. 가시 enabled 요소의 처음/끝 순환을 추가한 뒤 양쪽 viewport에서 통과했다.
  • 기존 degraded 테스트는 React element가 곧 native button이라는 전제를 두었다. UiButton의 실제 서버 렌더 결과를 확인하도록 바꾸고 문구·재시도·인증 분리 검증은 유지했다. 해당 테스트는 감사 manifest 미등재이며 manifest는 변경하지 않았다.
  • 최초 장부 검사에서 기존 미분류 14개(G02·G03·D03·#1277 잔여)를 발견해 U01 신규 경로만 분류하고 HQ에 전달했다. 이후 main 947413d1(G04·자동 랜딩·A01)을 합류할 때 해당 기준선도 정리되어 장부 검사가 통과했다.
  • main 합류 시 문서 목록·사이드바·생성 장부 3파일이 충돌했다. 목록/사이드바는 양쪽 항목을 모두 유지하고 장부는 합쳐진 규칙에서 재생성했다. U01의 UI 코드에는 충돌이 없었다.

5. 적용 결과

항목결과
공통 상태 계약개별 표시 판단 → 입력/일반 상태/저장 단계의 typed 표현 계약 v1
저장 표시기기 보관/보류/차단·서버 저장/통계 대기·통계 반영·unknown의 7가지 고정 의미
Node 신규 행동 검사10/10 통과; 관련 import boundary 포함 34/34
실제 Edge모바일 390×844·데스크톱 1440×1000, 8종씩 16/16 통과
대표 production 연결오류 gate 버튼 3개, 기존 배치·문구 유지
CSS기존 layer 5개 유지, base.primitives 1개, 기존 플랫폼 값·하단 clearance 보존
전체 check/build/CI·staging최종 SHA와 실행 결과는 #1289 완료 댓글 참조
커버리지 장부최초 기존 미분류 14개 → main 합류·U01 경로 등록 뒤 미분류 0, 검사 통과
실기기·운영 배포미검증/미배포. 실제 IME·VoiceOver/TalkBack·native back은 U06/N01, Production은 R06

6. 이번 개선으로 향상된 것

기능 담당이 표시 의미를 다시 해석할 필요가 줄었다

입력 raw와 상태, 저장 단계의 주입 shape, 각 intent와 복귀 경로가 하나의 계약에 있다. 다음 화면은 그 앵커에 맞춰 mapper와 controller를 연결하고, primitive 안에 업무 판단을 복제하지 않는다.

키보드와 CSS 경계의 검증 사례가 남는다

기본 Enter/IME/Tab/Escape 동작과 validation 연결을 실제 브라우저에서 실행할 수 있다. 의미 token·selector·overlay 예외의 소유권 목록은 U07이 사용 흔적을 지우기 전에 확인할 근거가 된다.

남은 것

  • U02/U03: 실제 editor/resource/mutation의 표시 모델 배선, 기존 dialog 계열의 화면별 이식. 기존 document Escape listener와 신규 top-layer modal을 혼합 중첩하지 않는다.
  • A12/A13·A08/A10·S 계열: InputCell/receipt/generation에서 표시 결론을 만드는 mapper와 동작 테스트.
  • U07: 기존 CSS·override·important 정리. 이 트랙은 일괄 삭제하지 않았다.
  • U06/N01: 실제 OS IME, screen reader, 확대 글자, native back, owner 전환, 중첩 overlay/가상 목록 focus 검증.
  • HQ: 총괄 U01 상태·Phase 집계 통합. Production 공개는 별도 릴리스 절차다.