MAKITTDocs

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.localhostmyshop
개발 (localhost)localhost?shop= 쿼리 파라미터 또는 기본값 demolocalhost:3003?shop=myshopmyshop
프로덕션*.makitt.shop서브도메인 추출myshop.makitt.shopmyshop
커스텀 도메인기타전체 hostname 사용my-store.commy-store.com

추출된 shopUrlx-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 값조건
1Customer 개인 설정CUSTOMER인증됨 + CustomerI18n 존재
2GeoIP + Accept-LanguageGEOLOCATIONGeoIP DB 가용
3Accept-Language만GEOLOCATIONGeoIP 불가
4Shop 기본값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/urlsShop 컨텍스트 없음

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 동작 모드

상태MSWshopIdResource/Action 호출
로컬 템플릿 모드항상 활성'mock'MSW가 mock 응답
API 모드 (Shop 미연결)활성 (토글 가능)'mock'MSW가 mock 응답
API 모드 (Shop 연결됨)비활성실제 shopId실제 shop-api 호출

Shop 앱과의 HCS 차이

Shop 앱Builder
HCS URLcompiledShopHcsUrl (토큰/프리셋 해석 완료)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