108 lines
10 KiB
Markdown
108 lines
10 KiB
Markdown
# Theme JSON 운영과 독립 배포
|
|
|
|
## Repository와 배포 단위
|
|
|
|
하나의 Git Repository에서 소스·Theme·Storybook·문서를 관리한다. UI, CENTER Graphic, Theme JSON은 각각 별도 배포 단위다. 현재 두 패키지는 `private: false`이며 compiled dist(JS·타입·CSS)를 export한다. Library build는 `npm run build:packages`로 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 테스트 ZIP 실행에는 Registry 접근·publish가 필요하지 않다. Theme JSON의 독립 산출 경로는 구현했다. 실제 CDN URL·인프라·인증·CI publish는 미정이다.
|
|
|
|
운영 배포 경계는 `packages/ui` → UI package, `packages/center-graphic` → Graphic package, `themes/*.json` → Theme CDN이다. **Git push ≠ Theme CDN publish**다. 운영 CI/CD는 JSON 검증·독립 artifact 산출 후 CDN에 배포하는 별도 단계로 구성하며, 현재 저장소에는 실제 CDN publish pipeline이 구현되어 있지 않다.
|
|
|
|
```text
|
|
HMDesignSystem/
|
|
├─ packages/
|
|
│ ├─ ui/ # @hm-design/ui: logic, contract, mapping, runtime
|
|
│ └─ center-graphic/ # 독립 Graphic package
|
|
├─ themes/
|
|
│ ├─ default.json # 92개 public color 값의 공식 Default 원본
|
|
│ ├─ service-example.json # 5개 역할만 바꾸는 명시적 예제
|
|
│ └─ drafts/ # 과거 초안; Theme 산출 대상 아님
|
|
├─ scripts/
|
|
│ ├─ theme-templates/ # 컬러 literal 없이 alias/geometry만 소유
|
|
│ ├─ theme-defaults.mjs # JSON -> package CSS/default 데이터 생성
|
|
│ └─ theme-artifacts.mjs # JSON validation -> 독립 CDN artifact
|
|
├─ src/theme/ # Storybook Preview/Editor/verification 소비자
|
|
└─ docs/guidelines/
|
|
```
|
|
|
|
`themes/default.json`은 기존 v1 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. Component Token은 없다. 94개 Runtime 입력 중 92개 컬러를 포함한다. 나머지 두 radius와 layout은 패키지 template에서 관리한다. 서비스 Theme는 같은 계약의 부분 컬러 override다.
|
|
|
|
## 단일 Default 원본과 package fallback
|
|
|
|
사람이 관리하는 컬러 값은 `themes/*.json`이다. Default JSON과 `scripts/theme-templates`에서 `packages/ui/src/theme/primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. Template는 기존 alias/geometry 정책을 유지하고 HEX literal을 따로 관리하지 않는다. JSON의 역할값이 팔레트 alias의 값과 같으면 기존 alias를 유지하고, 달라지면 JSON의 역할값을 우선한다.
|
|
|
|
패키지는 생성된 CSS fallback snapshot을 포함하므로 네트워크 Theme가 없거나 실패해도 기본 화면을 렌더링한다. 이미 배포한 UI package의 fallback은 **그 패키지 버전에서 생성한 HM Default snapshot**이다. 최신 운영 Default JSON을 CDN에서 로드하면 UI 재배포 없이 새 값이 적용된다. 네트워크 장애에서 최신 CDN 값까지 보장하는 시스템은 아니며, 그 경우 패키지 snapshot으로 복원한다.
|
|
|
|
Theme-only CDN 배포는 `npm run build:themes`만 실행한다. UI 재빌드·version up·registry publish가 필요 없다. 이후 UI 패키지를 새로 유지보수/검증할 때는 `npm run generate:theme-defaults`로 repo의 생성 snapshot을 갱신한다. `typecheck/build/build-storybook`의 pre hook은 JSON/template/생성 snapshot의 불일치를 검사한다. 이 검사는 Theme artifact 산출의 선행 조건이 아니다.
|
|
|
|
## 개발팀 Runtime 적용
|
|
|
|
설치/초기 서비스 연결 시 UI Component와 CSS를 가져온다. 실제 `themeUrl`은 서비스 설정에서 결정하며 package 내부에는 회사 CDN 주소가 없다.
|
|
|
|
```ts
|
|
import "@hm-design/ui/theme.css"
|
|
import {
|
|
fetchRuntimeTheme, resolveRuntimeTheme, loadRuntimeTheme,
|
|
applyRuntimeTheme, resetRuntimeTheme,
|
|
} from "@hm-design/ui/theme"
|
|
|
|
// serviceConfig.themeUrl은 CDN/API 설정값이다.
|
|
const root = document.documentElement
|
|
const success = await loadRuntimeTheme(root, serviceConfig.themeUrl)
|
|
// success=false: 이 root의 override를 해제하고 host/package HM fallback 유지.
|
|
|
|
// 별도의 Default JSON + 서비스 partial을 사용할 때:
|
|
try {
|
|
const defaults = await fetchRuntimeTheme(serviceConfig.defaultThemeUrl)
|
|
const serviceTheme = await fetchRuntimeTheme(serviceConfig.themeUrl)
|
|
applyRuntimeTheme(root, resolveRuntimeTheme(serviceTheme, defaults))
|
|
} catch {
|
|
resetRuntimeTheme(root)
|
|
}
|
|
|
|
// 이미 받은 설정/API/로컬 JSON에도 같은 Apply 계약을 사용한다.
|
|
// Apply에는 unknown 전체 validation이 있다.
|
|
applyRuntimeTheme(root, configurationTheme)
|
|
|
|
// 설정 URL의 버전/hash가 바뀌거나 운영 refresh 이벤트가 오면 다시 호출한다.
|
|
// 라이브러리/Component 재빌드와 브라우저 reload는 필요 없다.
|
|
await loadRuntimeTheme(root, serviceConfig.updatedThemeUrl)
|
|
```
|
|
|
|
- `fetchRuntimeTheme(url, RequestInit?)`: HTTP status/JSON/기존 allowlist 검증 후 partial 문서 반환. DOM을 건드리지 않고 실패 시 throw한다. URL/캐시/credentials/signal은 소비 서비스에서 지정한다.
|
|
- `resolveRuntimeTheme(theme, defaults?)`: 패키지 Default + 선택적 supplied Default + partial override를 검증 후 합친다. Component Token을 만들지 않는다. 그룹의 일부 stop만 제공하는 입력은 거부한다.
|
|
- `loadRuntimeTheme(root,url,{request?,defaults?}?)`: fetch/검증/선택적 merge/Apply의 편의 함수. 오류/invalid/HTTP 실패 시 false와 package fallback. 더 늦게 도착한 이전 요청은 새 Theme를 덮거나 reset하지 않는다. 직접 Apply/Reset도 pending load를 무효화한다.
|
|
- `isRuntimeTheme`: DOM 변경 없는 사전 검증. `applyRuntimeTheme`/`resetRuntimeTheme`: 기존 public Apply/Reset 경로와 host inline 값/priority 복원 정책 유지.
|
|
|
|
앱 시작 전/후 어디서든 호출할 수 있다. CSS는 최초 paint 전에 로드한다. 네트워크 응답 전에는 package fallback, 응답 후에는 External Theme다. 로딩 timeout이 필요하면 서비스에서 `AbortSignal.timeout(...)` 또는 AbortController를 RequestInit에 제공한다. 로더는 polling/push subscription을 자동 시작하지 않는다. 이미 열린 서비스가 최신 JSON을 쓰려면 소비 서비스의 refresh 이벤트/설정 갱신/호출 정책이 필요하다.
|
|
|
|
## 디자인팀 workflow
|
|
|
|
1. `themes/service-example.json`을 서비스 파일로 복사하거나 `themes/default.json`의 상위 컬러 역할을 수정한다. 공통 팔레트/gradient보다 Primary/Active/Focus/Accent 같은 역할을 우선 편집한다.
|
|
2. Storybook의 `Theme/Designer Playground/Editor`에서 URL/파일 Import로 JSON을 불러온다. 기본값은 `themes/default.json`을 Runtime merge하여 표시한다. 개발 서버에서는 Default JSON import의 HMR Preview도 가능하다. 다른 서비스 파일을 변경한 뒤 같은 URL을 Load URL로 다시 불러오면 CSS/Component 수정 없이 즉시 반영한다.
|
|
3. 실제 Input/Control/Calendar/Date Picker/메뉴/표/내비게이션과 1280/768/390 레이아웃을 확인한다. Editor의 값 변경도 같은 공식 Contract를 사용한다.
|
|
4. Editor를 이용했다면 Export한 JSON을 해당 `themes/*.json`에 저장해 Git에 포함한다. Editor는 repository 파일을 자동으로 덮어쓰지 않는다.
|
|
5. Git diff/review 후 디자인팀의 승인된 운영 절차로 commit/push한다.
|
|
6. 아래 Theme-only 산출물을 CDN에 배포한다. Git은 이력/검토, CDN은 Runtime delivery를 담당한다. 서비스가 Git 파일을 직접 읽지 않는다.
|
|
|
|
## Theme-only 배포
|
|
|
|
```powershell
|
|
npm run build:themes
|
|
# 결과: theme-dist/themes/default.json, service-example.json 및 추가한 top-level JSON
|
|
```
|
|
|
|
전체 문서 validation을 통과한 뒤에만 산출한다. `themes/drafts`는 제외한다. 삭제된 source의 옛 top-level JSON artifact도 출력 디렉터리에서 제거한다. 산출물은 `theme-dist/themes/`이며 UI/Graphic bundle과 별개다. 이 디렉터리의 파일만 선택한 CDN origin에 업로드하고 서비스 설정의 Theme URL을 갱신한다. CDN infrastructure/publish credentials/자동 pipeline은 구현하지 않았다. CDN은 cross-origin 사용을 위한 CORS 응답과 JSON Content-Type을 설정해야 한다.
|
|
|
|
## Cache/버전/롤백 권장안
|
|
|
|
인프라 미정 상태의 **추천안**은 `/themes/service-a/<content-hash>.json` 또는 `/themes/service-a/v12.json`처럼 변경마다 별도 파일을 발행하고 서비스 설정을 새 URL로 바꾸는 방식이다. 이전 파일을 유지하므로 rollback은 이전 URL로 돌린 후 Loader를 다시 호출한다. 버전 파일에는 장기 `Cache-Control: public, max-age=31536000, immutable`을 사용할 수 있다. AWS도 versioned 파일/디렉터리 이름으로 갱신하는 방식을 안내한다. [CloudFront 파일 versioning](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html), [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control).
|
|
|
|
대안은 `/themes/service-a.json?v=<content-hash>`다. CDN cache key가 query를 포함하도록 설정해야 효과가 있다. 안정 URL을 덮어쓰려면 짧은 TTL/`no-cache`와 ETag 재검증 또는 CDN purge 정책을 함께 정한다. 브라우저의 `fetch(...,{cache:"no-store"})`만으로 CDN edge 캐시를 항상 갱신한다고 보장하지 않는다. Preview는 no-store로 요청하지만 Production 캐시 정책은 소비 서비스/CDN 설정의 책임이다. [MDN HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching), [MDN Request.cache](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache).
|
|
|
|
실제 운영 URL/versioning 방식은 서비스 설정에서 확정한다. 자동 버전 관리/manifest/publish/rollback 서비스는 미구현이다.
|
|
|
|
## 보안/제한
|
|
|
|
공개 allowlist의 HEX6/기존 radius 값만 Production Runtime이 허용한다. Theme artifact와 Designer UI는 컬러 전용이며 Component Token/임의 CSS/HTML/JS/URL 값은 거부한다. invalid Import는 마지막 유효 Preview를 유지하고 URL Loader 실패는 package HM Default로 돌아간다. Partial은 교체이며 이전 Theme key가 남지 않는다.
|
|
|
|
Global root Theme는 body Portal에도 적용된다. wrapper 밖 Portal은 Global을 사용하며 Scoped 전달은 기존 FOLLOW_UP이다. 동일 root는 한 owner가 관리한다. 새 상위 역할/Mapping/behavior가 필요한 변경은 UI package 작업이며 단순 Theme 컬러 변경과 구분한다.
|