Shop Runtime — @makitt/api + @makitt/renderer 호출 흐름
docs/domain/shop-runtime.md
Shop Runtime — @makitt/api + @makitt/renderer 호출 흐름
개요
Shop 앱(makitt-shop)과 Builder 앱이 @makitt/api + @makitt/renderer를 통해 shop-api 서버와 통신하는 런타임 흐름 문서. 어떤 Shop이든 진입 시 자동으로 발생하는 HCS 데이터 로딩, 인증, i18n 결정, 페이지 렌더링 과정을 정리합니다.
1. Shop 앱 진입 전체 흐름
사용자가 Shop URL 접속
│
▼
┌─────────────────────────────────────────────────┐
│ Step 0. Middleware (Edge Runtime) │
│ 도메인 → shopUrl 추출 → 쿠키에 저장 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Step 1. Root Layout (Server Component) │
│ shopUrl → PublishedShopResponse 조회 │
│ → compiledShopHcsUrl에서 HCS JSON 다운로드 │
│ → CSS (tokens, styles) <head>에 삽입 │
│ → Shop 데이터를 Providers에 전달 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Step 2. AuthProvider (Client Component) │
│ auth/me → 인증 확인 │
│ → 미인증 시 auth/guest → 게스트 토큰 발급 │
│ → i18n.language 기반 UI 언어 자동 전환 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Step 3. 페이지 렌더링 │
│ URL path → Page 매칭 → TreeNode 렌더 │
│ → ResourceRef 바인딩 데이터 조회 │
│ → Event/Action 바인딩 │
└─────────────────────────────────────────────────┘
Step 0. 도메인 → shopUrl 해석 (Middleware)
middleware.ts (Edge Runtime)가 요청 도메인에서 shop 식별자를 추출합니다.
shopUrl 추출 전략
| 환경 | 도메인 패턴 | 추출 방식 | 예시 |
|---|---|---|---|
| 개발 | *.makitt.localhost | 서브도메인 추출 | myshop.makitt.localhost → myshop |
| 개발 (localhost) | localhost | ?shop= 쿼리 파라미터 또는 기본값 demo | localhost:3003?shop=myshop → myshop |
| 프로덕션 | *.makitt.shop | 서브도메인 추출 | myshop.makitt.shop → myshop |
| 커스텀 도메인 | 기타 | 전체 hostname 사용 | my-store.com → my-store.com |
추출된 shopUrl은 x-shop-url 쿠키에 저장되어 이후 서버 컴포넌트에서 접근 가능합니다.
Step 1. HCS 데이터 로딩 (Server Component)
Root Layout이 async Server Component로 HCS 데이터를 로드합니다. 클라이언트에 HTML이 도착하기 전에 완료됩니다.
1-1. Shop 메타데이터 조회
GET /api/v1/public/shops/url/{shopUrl}
서버: PublicShopController (makitt-api, 인증 불필요)
응답 (PublishedShopResponse):
{ "shopId": "shop-abc123", "shopName": "My Store", "shopUrl": "myshop", "language": "ko", "timezone": "Asia/Seoul", "currency": "KRW", "versionId": "ver-001", "publishedAt": "2025-01-15T00:00:00Z", "compiledShopHcsUrl": "https://cdn.../shop-abc123/compiled.json", "compiledTokensCssUrl": "https://cdn.../shop-abc123/tokens.css", "compiledStylesCssUrl": "https://cdn.../shop-abc123/styles.css" }
핵심 URL 3개:
compiledShopHcsUrl— Shop 전체 구조 (TreeNode 트리)compiledTokensCssUrl— 디자인 토큰 CSS 변수compiledStylesCssUrl— 컴포넌트 스타일 CSS
1-2. HCS JSON 다운로드
compiledShopHcsUrl에서 Shop 객체를 다운로드합니다.
interface Shop { shopId: string; name: string; header?: TreeNode; // 글로벌 헤더 footer?: TreeNode; // 글로벌 푸터 modals?: TreeNode[]; // 모달 정의 toast?: TreeNode; // 토스트 컨테이너 pages: Page[]; // 모든 페이지 i18n: { defaultLanguage: LanguageCode; supportedLanguages: LanguageCode[]; }; settings?: { maxWidth?: string; breakpoints?: ShopBreakpoints; }; state?: StateDefinition[]; // 글로벌 상태 resources?: ResourceRef[]; // 글로벌 리소스 } interface Page { pageId: string; title: string; path: string; // URL path 패턴 (path-to-regexp) content: TreeNode[]; // 페이지 컴포넌트 트리 meta?: PageMeta; // SEO 메타 state?: StateDefinition[]; // 페이지 상태 resources?: ResourceRef[]; // 페이지 리소스 guard?: PageGuard; // 접근 제어 }
1-3. CSS 로딩 + AppShell 구성
Root Layout (Server)
├─ <head>
│ ├─ <link> compiledTokensCssUrl ← 디자인 토큰 CSS 변수
│ └─ <link> compiledStylesCssUrl ← 컴포넌트 스타일
│
└─ <body>
└─ <Providers shop={shop}>
└─ <AppShell>
├─ <ModalContainer /> ← shop.modals
├─ <ToastContainer /> ← shop.toast
├─ <Header /> ← shop.header
├─ {children} ← 페이지 콘텐츠
└─ <Footer /> ← shop.footer
캐싱: ISR (Incremental Static Regeneration), 60초 revalidation
Step 2. 인증 + i18n (Client Component)
HTML이 클라이언트에 도착한 후 AuthProvider가 마운트되어 인증 흐름을 시작합니다.
2-1. 인증 초기화
// AuthProvider.tsx useEffect(() => { if (!hasInitialized.current && status === 'idle') { hasInitialized.current = true; initializeAuth(); } }, []);
2-2. API 호출 시퀀스
1. GET /shop/{shopId}/auth/me
├─ Cookie: accessToken=<있으면 전송>
└─ 응답: MeResponse 또는 error.auth.not_authenticated
2. (미인증 시) POST /shop/{shopId}/auth/guest
├─ 서버가 Set-Cookie: accessToken, refreshToken
└─ customerId를 localStorage에 저장
3. (게스트 발급 후) GET /shop/{shopId}/auth/me ← 재호출
└─ 응답: 게스트 MeResponse (i18n 포함)
2-3. i18n 자동 적용
인증 완료 후 auth/me 응답의 i18n.language를 기반으로 UI 언어 전환:
useEffect(() => { if (status === 'authenticated' && user?.i18n) { const language = user.i18n.language; if (language && isLanguageSupported(language)) { setLanguage(language); } } }, [status, user]);
핵심: 모든 방문자는 즉시 인증됨
- 기존 쿠키 → 기존 세션 유지 (CUSTOMER 또는 GUEST)
- 쿠키 없음 → 게스트 토큰 자동 발급 → GUEST 세션 생성
- 결과: Step 3 페이지 렌더링 시점에는 항상 인증된 상태
Step 3. 페이지 렌더링 (TreeNode → React)
3-1. URL → Page 매칭
URL: /products/123/reviews
→ shop.pages 배열 순회
→ page.path 패턴 매칭 (path-to-regexp)
→ 매칭된 page.content (TreeNode[]) 렌더링
3-2. Resource 바인딩 (읽기)
TreeNode에 ResourceRef가 바인딩되어 있으면 자동으로 API 호출:
TreeNode.resource = { key: 'product.products', params: { page: 0 } }
→ ResourceRegistry에서 fetcher 조회
→ productApi.list(shopId, { page: 0 })
→ TanStack Query 캐싱
→ 응답 데이터를 BindingContext에 주입
→ TreeNode가 바인딩된 데이터로 렌더
3-3. Action 바인딩 (쓰기)
TreeNode에 이벤트가 바인딩되어 있으면 사용자 인터랙션 시 Action 실행:
사용자 클릭/제출 → EventHandler
→ executeAction('cart.add', { productId, quantity })
→ ActionRegistry에서 handler 조회
→ cartApi.addItem(shopId, params)
→ 성공/실패 콜백
서버: ShopContextFilter (모든 요청 인터셉트)
클라이언트의 모든 /shop/{shopId}/** 요청은 ShopContextFilter를 통과합니다.
HTTP 요청 도착 (/shop/{shopId}/**)
│
├─ 1. shopId 추출 (URL 경로 regex)
├─ 2. Shop 엔티티 로드 → ShopLocalization (i18n 폴백용)
├─ 3. Access Token 추출
│ Priority: Authorization 헤더 > accessToken 쿠키
├─ 4. Token 검증 + shopId 일치 확인
│
├─ 5. ShopContext 생성 (ThreadLocal)
│ ├─ [인증됨] Customer 로드 → I18n 추론
│ └─ [미인증] I18n 추론 (GeoIP > Shop)
│
├─ 6. 필터 체인 → Controller
└─ 7. Finally: ThreadLocal 정리
I18n 추론 우선순위
| 순위 | 소스 | source 값 | 조건 |
|---|---|---|---|
| 1 | Customer 개인 설정 | CUSTOMER | 인증됨 + CustomerI18n 존재 |
| 2 | GeoIP + Accept-Language | GEOLOCATION | GeoIP DB 가용 |
| 3 | Accept-Language만 | GEOLOCATION | GeoIP 불가 |
| 4 | Shop 기본값 | SHOP_DEFAULT | 위 모든 소스 불가 시 |
상위 소스에서 누락된 필드는 항상 Shop 기본값으로 채워짐. null 없이 응답 보장.
Builder 앱 진입 흐름
Builder는 자체 인증 체계(makitt-api 기반)를 가지며, 진입 경로에 따라 Shop API 연동 방식이 달라집니다.
진입 모드
| 경로 | 모드 | HCS 데이터 소스 | Shop API |
|---|---|---|---|
/[template] | 로컬 템플릿 | 로컬 /api/templates/{templateId} | MSW mock (setShopId('mock')) |
/shop/[shopId]/version/[versionId] | API 모드 | 서버 shopHcsUrl (S3) | MSW 또는 실제 API (토글 가능) |
/system-default/theme | 시스템 기본값 | 서버 /api/v1/shops/system-defaults/urls | Shop 컨텍스트 없음 |
API 모드 진입 흐름 (/shop/{shopId}/version/{versionId})
Builder에서 Shop 편집 진입
│
▼
┌─────────────────────────────────────────────────┐
│ 1. shopId, versionId를 URL params에서 추출 │
│ setShopId(shopId) ← @makitt/api에 shopId 설정│
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2. Shop + Version 데이터 로드 │
│ POST /api/v1/shops/{shopId}/versions/ │
│ {versionId}/detail │
│ → ShopWithVersionResponse { shop, version } │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3. HCS 다운로드 (version의 S3 URL) │
│ ├─ shopHcsUrl → TreeNode 구조 (raw, 미컴파일) │
│ ├─ tokensUrl → 디자인 토큰 │
│ ├─ presetsUrl → 컴포넌트 프리셋 │
│ └─ nodeDefaultsUrl → 노드 기본값 │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 4. Store 로딩 │
│ loadTemplate(), loadTokens(), loadPresets() │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 5. MSWProvider 판단 │
│ ├─ Shop 미연결 → MSW 활성, setShopId('mock') │
│ └─ Shop 연결됨 → MSW 비활성, │
│ setShopId(connectedShopId) │
│ setApiConfig(apiBaseUrl) │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 6. Canvas 렌더링 │
│ TreeRenderer로 편집 가능한 TreeNode 렌더 │
│ → Resource 호출 (MSW mock 또는 실제 shop-api) │
└─────────────────────────────────────────────────┘
MSW 동작 모드
| 상태 | MSW | shopId | Resource/Action 호출 |
|---|---|---|---|
| 로컬 템플릿 모드 | 항상 활성 | 'mock' | MSW가 mock 응답 |
| API 모드 (Shop 미연결) | 활성 (토글 가능) | 'mock' | MSW가 mock 응답 |
| API 모드 (Shop 연결됨) | 비활성 | 실제 shopId | 실제 shop-api 호출 |
Shop 앱과의 HCS 차이
| Shop 앱 | Builder | |
|---|---|---|
| HCS URL | compiledShopHcsUrl (토큰/프리셋 해석 완료) | shopHcsUrl (raw, 미컴파일) |
| 토큰/프리셋 | CSS 파일로 <head> 삽입 | 별도 URL로 다운로드 후 Store에 로딩 |
| 용도 | 최종 소비자 렌더링 | 편집기 캔버스 렌더링 |
@makitt/api 클라이언트 구성
Base URL 결정 우선순위
1. setApiConfig()로 명시 설정 (Builder가 프록시 URL 지정)
2. MSW 활성화 → '' (MSW가 인터셉트)
3. SSR → process.env.NEXT_PUBLIC_SHOP_API_BASE_URL
4. CSR → '' (same-origin)
인증 방식
- HTTP-only 쿠키 기반 (
accessToken,refreshToken) - 모든 요청에
credentials: 'include'자동 설정 - SSR 시에는
Cookie: accessToken=<token>헤더 직접 전달
URL 패턴
| 도메인 | Base Path |
|---|---|
| Auth, Blog, Notice, Newsletter | /shop/{shopId}/... |
| Product, Cart | /api/v1/shops/{shopId}/... |
핵심 API 스펙
GET /api/v1/public/shops/url/{shopUrl} — Shop 메타데이터 (인증 불필요)
호출 시점: Shop 앱 Step 1 (SSR)
서버: PublicShopController (makitt-api)
{ "shopId": "shop-abc123", "shopName": "My Store", "shopUrl": "myshop", "description": "Store description", "logoUrl": "https://cdn.../logo.png", "bannerImageUrl": "https://cdn.../banner.png", "language": "ko", "timezone": "Asia/Seoul", "currency": "KRW", "seoTitle": "My Store", "seoDescription": "...", "seoImageUrl": "https://cdn.../og.png", "versionId": "ver-001", "versionName": "v1.0", "publishedAt": "2025-01-15T00:00:00Z", "shopHcsUrl": "https://cdn.../raw.json", "tokensUrl": "https://cdn.../tokens.json", "presetsUrl": "https://cdn.../presets.json", "nodeDefaultsUrl": "https://cdn.../node-defaults.json", "compiledShopHcsUrl": "https://cdn.../compiled.json", "compiledTokensCssUrl": "https://cdn.../tokens.css", "compiledStylesCssUrl": "https://cdn.../styles.css" }
POST /api/v1/shops/{shopId}/versions/{versionId}/detail — Shop + Version 상세
호출 시점: Builder API 모드 진입
서버: ShopVersionController (makitt-api, Builder 인증 필요)
{ "shop": { "shopId": "shop-abc123", "userId": "user-001", "organizationId": "org-001", "shopName": "My Store", "shopUrl": "myshop", "description": "...", "logoUrl": "https://cdn.../logo.png", "bannerImageUrl": null, "language": "ko", "timezone": "Asia/Seoul", "currency": "KRW", "seoTitle": "My Store", "seoDescription": "...", "seoImageUrl": null, "localizationConfirmed": true, "markets": ["KR", "US"], "primaryMarketCode": "KR", "customDomain": null, "status": { "name": "ACTIVE", "description": "..." }, "createdAt": "2025-01-01T00:00:00Z", "updatedAt": "2025-01-15T00:00:00Z", "initialVersion": null }, "version": { "versionId": "ver-001", "shopId": "shop-abc123", "versionName": "v1.0", "description": "...", "shopHcsUrl": "https://cdn.../raw.json", "tokensUrl": "https://cdn.../tokens.json", "presetsUrl": "https://cdn.../presets.json", "nodeDefaultsUrl": "https://cdn.../node-defaults.json", "compiledShopHcsUrl": "https://cdn.../compiled.json", "compiledTokensCssUrl": "https://cdn.../tokens.css", "compiledStylesCssUrl": "https://cdn.../styles.css", "status": { "name": "DRAFT", "description": "..." }, "metadata": {}, "createdAt": "2025-01-01T00:00:00Z", "updatedAt": "2025-01-15T00:00:00Z", "publishedAt": null } }
GET /api/v1/shops/system-defaults/urls — 시스템 기본값 URL
호출 시점: Builder system-default 모드
서버: ShopController (makitt-api)
{ "tokensUrl": "https://cdn.../system/tokens.json", "presetsUrl": "https://cdn.../system/presets.json", "nodeDefaultsUrl": "https://cdn.../system/node-defaults.json" }
GET /shop/{shopId}/auth/me — 현재 고객 정보 + i18n
호출 시점: Shop 앱 Step 2 (CSR), Builder API 모드 (Shop 연결 시)
서버: AuthController (makitt-shop-api, ShopContextFilter 통과)
{ "customerId": "cust_abc123xyz", "shopId": "shop-abc123", "email": "user@example.com", "nickname": "user", "profileUrl": null, "customerType": { "name": "CUSTOMER", "description": "등록된 고객" }, "authProvider": { "name": "EMAIL_PASSWORD", "description": "이메일/비밀번호" }, "i18n": { "language": "ko", "timezone": "Asia/Seoul", "currency": { "code": "KRW", "symbol": "₩" }, "numberingSystem": "latn", "dateTimeFormat": "YYYY-MM-DD", "marketCode": "KR", "source": "CUSTOMER" } }
i18n.source 값: "CUSTOMER" | "GEOLOCATION" | "SHOP_DEFAULT"
POST /shop/{shopId}/auth/guest — 게스트 토큰 발급
호출 시점: Shop 앱 Step 2 (auth/me 실패 시 자동)
{ "customerId": "cust_guest789xyz", "shopId": "shop-abc123", "customerType": { "name": "GUEST", "description": "게스트 고객" }, "accessToken": "eyJhbG...", "refreshToken": "eyJhbG...", "expiresIn": 3600, "tokenType": "Bearer" }
POST /shop/{shopId}/auth/login/email-password — 로그인
// Request { "email": "user@example.com", "password": "..." } // Response { "customerId": "cust_abc123xyz", "shopId": "shop-abc123", "email": "user@example.com", "nickname": "user", "profileUrl": null, "customerType": { "name": "CUSTOMER", "description": "..." }, "authProvider": { "name": "EMAIL_PASSWORD", "description": "..." }, "accessToken": "eyJhbG...", "refreshToken": "eyJhbG...", "expiresIn": 3600, "tokenType": "Bearer" }
HCS JSON 구조 (compiledShopHcsUrl 응답)
{ "shopId": "shop-abc123", "name": "My Store", "header": { "id": "h1", "type": "header", "children": [...] }, "footer": { "id": "f1", "type": "footer", "children": [...] }, "modals": [], "toast": null, "pages": [ { "pageId": "page-001", "title": "Home", "path": "/", "meta": { "title": "My Store", "description": "...", "ogImage": "..." }, "content": [ { "id": "node-001", "parentId": null, "type": "container", "className": "css-abc123", "props": {}, "children": [ { "id": "node-002", "parentId": "node-001", "type": "text", "className": "css-def456", "children": "Welcome!", "props": { "as": "h1" } } ], "events": {}, "visible": true, "resource": { "type": "product", "id": "products" } } ], "state": [], "resources": [{ "key": "product.products", "params": { "size": 12 } }], "guard": null, "isActive": true } ], "i18n": { "defaultLanguage": "ko", "supportedLanguages": ["ko", "en", "ja"] }, "settings": { "maxWidth": "1200px", "breakpoints": { "mobile": { "maxWidth": 768, "label": "Mobile" }, "tablet": { "maxWidth": 1024, "label": "Tablet" } } }, "state": [], "resources": [] }
참조
- Domain: Shop — ShopLocalization, ShopOnboarding
- Domain: Market — 마켓별 설정
- Server:
makitt-server/makitt-shop-api— ShopContextFilter, I18nInferenceService - Server:
makitt-server/makitt-api— PublicShopController (HCS URL 제공) - Client:
makitt-client/apps/shop— Shop 앱 (HCS 기반 렌더) - Client:
makitt-client/packages/api/— ApiClient, Resource/Action Registry - Client:
makitt-client/packages/renderer/— TreeRenderer, BindingContextProvider, EventProvider